From 41458bbdd1bdaeaff3cb0f2d64dd6304f7bea6a4 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Fri, 2 Oct 2026 21:48:48 +0530 Subject: [PATCH 01/24] Centralize LLM Error Handling and Fallback Messages --- docs/mcp_error_handling.md | 65 +++++++++++++++++++++ internal/mcpagent/agent.go | 77 +++++-------------------- internal/mcpagent/analytics.go | 15 ++--- internal/mcpagent/provider.go | 100 +++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 72 deletions(-) create mode 100644 docs/mcp_error_handling.md diff --git a/docs/mcp_error_handling.md b/docs/mcp_error_handling.md new file mode 100644 index 000000000..5675eda5e --- /dev/null +++ b/docs/mcp_error_handling.md @@ -0,0 +1,65 @@ +# MCP LLM Error Handling + +When interacting with multiple AI Providers (Google, OpenAI, Anthropic) via `langchaingo`, LLM calls can fail in messy, unpredictable ways. Providers may return HTTP 503s for overload, 429s for rate limits, or 401s for revoked keys. + +If we return these raw errors directly to the MCP client (the chat UI), the turn crashes and the user sees ugly stack traces (e.g., `googleapi: Error 503`). + +To solve this, we use a **centralized UI fallback mechanism** that traps raw SDK errors and gracefully degrades the UI without disrupting the core ReAct loop. + +## The Architecture + +The error handling logic lives in `internal/mcpagent/provider.go` and consists of two functions: + +### 1. `CategorizeLLMError(err error) LLMErrorCategory` +This function intercepts an error, inspects it for `langchaingo` normalizations, unwraps leaked provider-specific errors (like `*googleapi.Error`), and falls back to string matching. + +It maps the messy error into one of four clean categories: +* `ErrCategoryAuth` (401, 403, 404, invalid keys, revoked models) +* `ErrCategoryOverload` (429, 503, rate limits, high demand) +* `ErrCategoryTimeout` (504, context deadline exceeded) +* `ErrCategoryUnknown` (Prompt parsing errors, network disconnects) + +### 2. `GetLLMFallbackMessage(cat LLMErrorCategory) string` +This function takes a category and returns a beautifully formatted, non-emoji Markdown string to show the user. +* If it returns a non-empty string, **the turn is considered saved**. We show the user this message. +* If it returns `""` (for `ErrCategoryUnknown`), the error is fatal and should be bubbled up. + +## How to use it + +We deliberately **do not** wrap the original `a.provider.Complete(...)` function. We keep the core LLM signatures untouched. Instead, you just "pick up" the error immediately after the LLM call fails. + +### Example (from `agent.go`) + +```go +response, usage, err := a.provider.Complete(ctx, history, tools, completeOpts...) +if err != nil { + // 1. Pick up the error and categorize it + errCategory := CategorizeLLMError(err) + + // 2. Ask for a clean UI message + msg := GetLLMFallbackMessage(errCategory) + + // 3. If there is no clean fallback, bubble up the fatal error + if msg == "" { + return "", history, nil, nil, fmt.Errorf("llm completion failed: %w", err) + } + + // 4. Otherwise, inject the fallback message as an AI response and return a nil error! + // This gracefully completes the turn and renders the message in the UI. + history = append(history, HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + }) + return msg, history, nil, nil, nil +} +``` + +## Adding new Fallback Scenarios + +If you need to handle a new failure mode (e.g. `ContextTooLarge`): +1. Add `ErrCategoryContextSize` to the const block in `provider.go`. +2. Add the detection logic to `CategorizeLLMError`. +3. Add the exact Markdown message you want the user to see to the `switch` block in `GetLLMFallbackMessage`. + +This guarantees every MCP LLM call across the app degrades consistently. diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index c55b7461f..9a07b0792 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -3,7 +3,6 @@ package mcpagent import ( "context" "encoding/json" - "errors" "fmt" "strings" "time" @@ -13,7 +12,6 @@ import ( "github.com/livereview/internal/vlrender" "github.com/rs/zerolog/log" "github.com/tmc/langchaingo/llms" - "google.golang.org/api/googleapi" ) const ( @@ -295,27 +293,27 @@ func (a *Agent) runStepLoop( if jsonMode { completeOpts = append(completeOpts, llms.WithJSONMode()) } + response, usage, err := a.provider.Complete(ctx, history, tools, completeOpts...) aiElapsed := time.Since(aiStart) if err != nil { - log.Error().Err(err).Int("step", step).Msg("LLM completion failed") + errCategory := CategorizeLLMError(err) + log.Error().Err(err).Int("step", step).Str("category", string(errCategory)).Msg("LLM completion failed") clog.AIError(callNumber, step, aiElapsed, err) - if isProviderAuthOrModelError(err) { - msg := fmt.Sprintf("> ⚠️ **Action Required: AI Provider Issue**\n> \n> The existing **%s**'s key seems expired/revoked or the model is no longer available.\n\nPlease configure a valid provider to continue: [btn:Configure AI Provider](/settings#ai)", a.provider.Describe()) - clog.FinalResponse(msg + " (stopped after provider auth/model error)") - - history = append(history, HistoryEntry{ - "role": "assistant", - "content": msg, - "text": msg, - "suggested_questions": DefaultAIErrorSuggestedQuestions, - }) - - return msg, history, nil, nil, nil + msg := GetLLMFallbackMessage(errCategory) + if msg == "" { + return "", history, nil, nil, fmt.Errorf("llm completion step %d: %w", step, err) } - return "", history, nil, nil, fmt.Errorf("llm completion step %d: %w", step, err) + clog.FinalResponse(msg + " (stopped after " + string(errCategory) + ")") + history = append(history, HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + "suggested_questions": DefaultAIErrorSuggestedQuestions, + }) + return msg, history, nil, nil, nil } log.Debug().Int("step", step).Int("response_len", len(response)).Msg("LLM call succeeded") clog.AIResponse(callNumber, step, aiElapsed, usage.InputTokens, usage.OutputTokens, response) @@ -599,56 +597,9 @@ func (a *Agent) interpretUserMessage(clog *logging.ChatTurnLogger, userText stri return msg, nil } -// isProviderAuthOrModelError reports whether an LLM completion error is caused -// by a revoked/expired AI provider key or a deprecated/unavailable model. -func isProviderAuthOrModelError(err error) bool { - if err == nil { - return false - } - - // 1. Check langchaingo standardized errors - var llmsErr *llms.Error - if errors.As(err, &llmsErr) { - if llmsErr.Code == llms.ErrCodeAuthentication || llmsErr.Code == llms.ErrCodeResourceNotFound { - return true - } - } - - // 2. Check Google API typed errors - var gErr *googleapi.Error - if errors.As(err, &gErr) { - if gErr.Code == 401 || gErr.Code == 403 || gErr.Code == 404 { - return true - } - } - // 3. Check generic interface methods for HTTP status codes (used by OpenAI client wrapper, etc.) - var scErr interface{ StatusCode() int } - if errors.As(err, &scErr) { - code := scErr.StatusCode() - if code == 401 || code == 403 || code == 404 { - return true - } - } - var hscErr interface{ HTTPStatusCode() int } - if errors.As(err, &hscErr) { - code := hscErr.HTTPStatusCode() - if code == 401 || code == 403 || code == 404 { - return true - } - } - // 4. Fallback for non-HTTP API responses indicating model unavailability - msg := strings.ToLower(err.Error()) - return strings.Contains(msg, "deprecated") || - strings.Contains(msg, "model not found") || - strings.Contains(msg, "status code: 401") || - strings.Contains(msg, "status code: 403") || - strings.Contains(msg, "status code: 404") || - strings.Contains(msg, "unauthorized") || - strings.Contains(msg, "forbidden") -} // isAuthError reports whether a tool result signals an expired/invalid // session token or missing auth - the exact messages LiveReview's own auth diff --git a/internal/mcpagent/analytics.go b/internal/mcpagent/analytics.go index ee5bb4392..b4d216a3e 100644 --- a/internal/mcpagent/analytics.go +++ b/internal/mcpagent/analytics.go @@ -42,7 +42,7 @@ const ( // analyticsTurnTimeout bounds the whole fan-out regardless of per-query // timeouts, so a slow model cannot hold a request open indefinitely. - analyticsTurnTimeout = 90 * time.Second + analyticsTurnTimeout = 300 * time.Second ) // AnalyticsEngine executes guard-rewritten SQL. Declared here rather than @@ -1651,16 +1651,13 @@ func (a *Agent) runMultiInterpret( raw, err := a.completeOnce(ctx, clog, 2, "interpret", "multi", 1, system, userMsg) if err != nil { log.Error().Err(err).Msg("multi-interpret LLM call failed") - if isProviderAuthOrModelError(err) { - msg := fmt.Sprintf("> ⚠️ **Action Required: AI Provider Issue**\n> \n> The existing **%s**'s key seems expired/revoked or the model is no longer available.\n\nPlease configure a valid provider to continue: [btn:Configure AI Provider](/settings#ai)", a.provider.Describe()) - clog.FinalResponse(msg + " (stopped after provider auth/model error)") + cat := CategorizeLLMError(err) + msg := GetLLMFallbackMessage(cat) + if msg != "" { + clog.FinalResponse(msg + " (stopped after " + string(cat) + ")") return msg, nil, debug, nil } - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - // Intentionally return nil error here so the agent caller treats this turn as a success - // and persists the friendly timeout message to the database, rather than throwing it away. - return "The analysis took longer than expected. Please try asking a narrower or more specific question.", nil, debug, nil //nolint:nilerr - } + return "I had trouble understanding that question. Please try rephrasing.", nil, debug, nil //nolint:nilerr } debug.LLMRawResponse = raw diff --git a/internal/mcpagent/provider.go b/internal/mcpagent/provider.go index 472635773..8323c4208 100644 --- a/internal/mcpagent/provider.go +++ b/internal/mcpagent/provider.go @@ -3,11 +3,14 @@ package mcpagent import ( "context" "encoding/json" + "errors" "fmt" + "strings" "github.com/livereview/internal/aiconnectors" "github.com/rs/zerolog/log" "github.com/tmc/langchaingo/llms" + "google.golang.org/api/googleapi" ) type Provider struct { @@ -186,3 +189,100 @@ func (p *Provider) historyToMessages(history []HistoryEntry) []llms.MessageConte } return messages } + +type LLMErrorCategory string + +const ( + ErrCategoryAuth LLMErrorCategory = "auth_failed" + ErrCategoryOverload LLMErrorCategory = "overloaded" + ErrCategoryTimeout LLMErrorCategory = "timeout" + ErrCategoryUnknown LLMErrorCategory = "unknown" +) + +// CategorizeLLMError maps raw SDK errors from Langchain or underlying clients +// into our standard failure categories for consistent UI fallback rendering. +func CategorizeLLMError(err error) LLMErrorCategory { + if err == nil { + return ErrCategoryUnknown + } + + // 1. Langchain-Go normalized errors + var llmsErr *llms.Error + if errors.As(err, &llmsErr) { + if llmsErr.Code == llms.ErrCodeAuthentication || llmsErr.Code == llms.ErrCodeResourceNotFound { + return ErrCategoryAuth + } + if llmsErr.Code == llms.ErrCodeRateLimit { + return ErrCategoryOverload + } + } + + // 2. Raw Google API errors (often leaked by langchaingo) + var gErr *googleapi.Error + if errors.As(err, &gErr) { + if gErr.Code == 401 || gErr.Code == 403 || gErr.Code == 404 { + return ErrCategoryAuth + } + if gErr.Code == 429 || gErr.Code == 500 || gErr.Code == 502 || gErr.Code == 503 || gErr.Code == 504 { + return ErrCategoryOverload + } + } + + // 3. Generic HTTP status codes + var scErr interface{ StatusCode() int } + if errors.As(err, &scErr) { + code := scErr.StatusCode() + if code == 401 || code == 403 || code == 404 { + return ErrCategoryAuth + } + if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { + return ErrCategoryOverload + } + } + + var hscErr interface{ HTTPStatusCode() int } + if errors.As(err, &hscErr) { + code := hscErr.HTTPStatusCode() + if code == 401 || code == 403 || code == 404 { + return ErrCategoryAuth + } + if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { + return ErrCategoryOverload + } + } + + // 4. Fallback string matching + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "context deadline") || strings.Contains(msg, "timeout") { + return ErrCategoryTimeout + } + if strings.Contains(msg, "rate limit") || strings.Contains(msg, "too many requests") || + strings.Contains(msg, "status code: 429") || strings.Contains(msg, "status code: 50") || + strings.Contains(msg, "service unavailable") || strings.Contains(msg, "bad gateway") || + strings.Contains(msg, "high demand") { + return ErrCategoryOverload + } + if strings.Contains(msg, "deprecated") || strings.Contains(msg, "model not found") || + strings.Contains(msg, "status code: 401") || strings.Contains(msg, "status code: 403") || + strings.Contains(msg, "status code: 404") || strings.Contains(msg, "unauthorized") || + strings.Contains(msg, "forbidden") { + return ErrCategoryAuth + } + + return ErrCategoryUnknown +} + +// GetLLMFallbackMessage returns a safe, beautiful UI response for the client +// based on the categorized LLM error. Returns empty string for unknown errors. +func GetLLMFallbackMessage(cat LLMErrorCategory) string { + switch cat { + case ErrCategoryAuth: + return "> **Action Required: AI Provider Issue**\n> \n> The AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.\n> \n> [btn:Configure AI Provider](/settings#ai)" + case ErrCategoryOverload: + return "> **AI Provider is Busy**\n> \n> The AI provider is currently experiencing high traffic. If you continue to see this issue, we suggest changing to a different model or provider." + case ErrCategoryTimeout: + return "> **Analysis Took Too Long**\n> \n> The AI took too long to generate your response and timed out. Try asking a narrower or more specific question." + default: + return "" + } +} From 0da1b169098badfeb26f9f4b54786fb80c54b6a3 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 3 Oct 2026 14:21:09 +0530 Subject: [PATCH 02/24] Add Centralized LLM Error Categorization and Action Cards --- internal/aiconnectors/errors.go | 87 +++++++++++++ internal/aiconnectors/errors_test.go | 101 +++++++++++++++ internal/api/chat_conversations_handler.go | 44 ++++--- internal/api/webchat_handler.go | 64 ++++++++-- internal/docindex/docs/.synced-commits.env | 4 +- internal/mcpagent/agent.go | 85 +++++++++++-- internal/mcpagent/analytics.go | 36 +++++- internal/mcpagent/provider.go | 141 +++++++-------------- internal/mcpagent/provider_test.go | 53 ++++++++ internal/mcpagent/types.go | 9 ++ ui/src/api/chatConversations.ts | 6 + ui/src/api/chatbot.ts | 7 + ui/src/pages/Chatbot/ChatConversation.tsx | 64 +++++----- 13 files changed, 524 insertions(+), 177 deletions(-) create mode 100644 internal/aiconnectors/errors.go create mode 100644 internal/aiconnectors/errors_test.go create mode 100644 internal/mcpagent/provider_test.go diff --git a/internal/aiconnectors/errors.go b/internal/aiconnectors/errors.go new file mode 100644 index 000000000..7c15883e1 --- /dev/null +++ b/internal/aiconnectors/errors.go @@ -0,0 +1,87 @@ +package aiconnectors + +import ( + "errors" + "strings" + + "github.com/tmc/langchaingo/llms" +) + +type LLMErrorCategory string + +const ( + ErrCategoryAuth LLMErrorCategory = "auth_failed" + ErrCategoryOverload LLMErrorCategory = "overloaded" + ErrCategoryTimeout LLMErrorCategory = "timeout" + ErrCategoryDeprecated LLMErrorCategory = "model_deprecated" + ErrCategoryUnknown LLMErrorCategory = "unknown" +) + +// CategorizeLLMError maps raw SDK errors from Langchain or underlying clients +// into our standard failure categories for consistent UI fallback rendering. +func CategorizeLLMError(err error) LLMErrorCategory { + if err == nil { + return ErrCategoryUnknown + } + + // 1. Langchain-Go normalized errors + var llmsErr *llms.Error + if errors.As(err, &llmsErr) { + if llmsErr.Code == llms.ErrCodeAuthentication || llmsErr.Code == llms.ErrCodeResourceNotFound { + return ErrCategoryAuth + } + if llmsErr.Code == llms.ErrCodeRateLimit { + return ErrCategoryOverload + } + } + + // 2. Generic HTTP status codes + var scErr interface{ StatusCode() int } + if errors.As(err, &scErr) { + code := scErr.StatusCode() + if code == 401 || code == 403 || code == 404 { + return ErrCategoryAuth + } + if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { + return ErrCategoryOverload + } + } + + var hscErr interface{ HTTPStatusCode() int } + if errors.As(err, &hscErr) { + code := hscErr.HTTPStatusCode() + if code == 401 || code == 403 || code == 404 { + return ErrCategoryAuth + } + if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { + return ErrCategoryOverload + } + } + + // 3. Fallback string matching + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "context deadline") || strings.Contains(msg, "timeout") { + return ErrCategoryTimeout + } + if strings.Contains(msg, "rate limit") || strings.Contains(msg, "too many requests") || + strings.Contains(msg, "status code: 429") || strings.Contains(msg, "status code: 50") || + strings.Contains(msg, "error 429") || strings.Contains(msg, "error 50") || + strings.Contains(msg, "service unavailable") || strings.Contains(msg, "bad gateway") || + strings.Contains(msg, "high demand") { + return ErrCategoryOverload + } + if strings.Contains(msg, "deprecated") || strings.Contains(msg, "model not found") || + strings.Contains(msg, "model_not_found") || strings.Contains(msg, "no such model") { + return ErrCategoryDeprecated + } + if strings.Contains(msg, "status code: 401") || strings.Contains(msg, "status code: 403") || + strings.Contains(msg, "status code: 404") || strings.Contains(msg, "unauthorized") || + strings.Contains(msg, "error 401") || strings.Contains(msg, "error 403") || + strings.Contains(msg, "error 404") || strings.Contains(msg, "forbidden") { + return ErrCategoryAuth + } + + return ErrCategoryUnknown +} + + diff --git a/internal/aiconnectors/errors_test.go b/internal/aiconnectors/errors_test.go new file mode 100644 index 000000000..849b2dc20 --- /dev/null +++ b/internal/aiconnectors/errors_test.go @@ -0,0 +1,101 @@ +package aiconnectors + +import ( + "errors" + "net/http" + "testing" + + "github.com/tmc/langchaingo/llms" +) + +// mockHTTPError mocks an error that provides a StatusCode() method. +type mockHTTPError struct { + code int + msg string +} + +func (e *mockHTTPError) Error() string { + return e.msg +} + +func (e *mockHTTPError) StatusCode() int { + return e.code +} + +func TestCategorizeLLMError(t *testing.T) { + tests := []struct { + name string + err error + want LLMErrorCategory + }{ + { + name: "Nil error", + err: nil, + want: ErrCategoryUnknown, + }, + { + name: "Langchain Auth Error", + err: &llms.Error{Code: llms.ErrCodeAuthentication}, + want: ErrCategoryAuth, + }, + { + name: "Langchain Rate Limit Error", + err: &llms.Error{Code: llms.ErrCodeRateLimit}, + want: ErrCategoryOverload, + }, + { + name: "HTTP 401 via StatusCode()", + err: &mockHTTPError{code: http.StatusUnauthorized, msg: "unauthorized"}, + want: ErrCategoryAuth, + }, + { + name: "HTTP 503 via StatusCode()", + err: &mockHTTPError{code: http.StatusServiceUnavailable, msg: "service unavailable"}, + want: ErrCategoryOverload, + }, + { + name: "String Match: context deadline", + err: errors.New("operation failed: context deadline exceeded"), + want: ErrCategoryTimeout, + }, + { + name: "String Match: high demand", + err: errors.New("googleapi: Error 503: This model is currently experiencing high demand."), + want: ErrCategoryOverload, + }, + { + name: "String Match: error 429", + err: errors.New("provider returned error 429 too many requests"), + want: ErrCategoryOverload, + }, + { + name: "String Match: model not found", + err: errors.New("model not found or deprecated"), + want: ErrCategoryAuth, + }, + { + name: "String Match: error 404", + err: errors.New("error 404: resource not found"), + want: ErrCategoryAuth, + }, + { + name: "String Match: error 502", + err: errors.New("googleapi: error 502: bad gateway"), + want: ErrCategoryOverload, + }, + { + name: "Unknown Error", + err: errors.New("something went completely wrong"), + want: ErrCategoryUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CategorizeLLMError(tt.err) + if got != tt.want { + t.Errorf("CategorizeLLMError() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/api/chat_conversations_handler.go b/internal/api/chat_conversations_handler.go index 70fc99d7a..6814aee9a 100644 --- a/internal/api/chat_conversations_handler.go +++ b/internal/api/chat_conversations_handler.go @@ -27,13 +27,14 @@ type ChatConversationSummary struct { // ChatMessageOut is one persisted message, with any charts and file exports // it produced. type ChatMessageOut struct { - ID int64 `json:"id"` - Role string `json:"role"` - Content string `json:"content"` - Charts []WebChatChart `json:"charts,omitempty"` - Files []WebChatFile `json:"files,omitempty"` + ID int64 `json:"id"` + Role string `json:"role"` + Content string `json:"content"` + Charts []WebChatChart `json:"charts,omitempty"` + Files []WebChatFile `json:"files,omitempty"` SuggestedQuestions []mcpagent.SuggestedQuestionCategory `json:"suggested_questions,omitempty"` - DebugArtifacts json.RawMessage `json:"debug_artifacts,omitempty"` + DebugArtifacts json.RawMessage `json:"debug_artifacts,omitempty"` + ActionCard *mcpagent.ActionCard `json:"action_card,omitempty"` } // ChatConversationDetail is a full conversation with its message history, for @@ -201,19 +202,29 @@ func (s *Server) GetConversation(c echo.Context) error { }) } var suggestedQuestions []mcpagent.SuggestedQuestionCategory + var actionCard *mcpagent.ActionCard for _, entry := range m.RawHistoryEntries { - if sq, ok := entry["suggested_questions"].([]mcpagent.SuggestedQuestionCategory); ok && len(sq) > 0 { - suggestedQuestions = sq - break - } else if rawSq, ok := entry["suggested_questions"]; ok && rawSq != nil { - if b, err := json.Marshal(rawSq); err == nil { - if err := json.Unmarshal(b, &suggestedQuestions); err != nil { - log.Warn().Err(err).Msg("failed to unmarshal raw suggested_questions") - } else if len(suggestedQuestions) > 0 { - break - } + // After JSONB round-trip from Postgres, all values come back as + // generic types ([]interface{}, map[string]interface{}, etc.), + // not as typed Go structs. Re-marshal each entry and read the + // fields we care about from a plain map so type assertions are safe. + entryBytes, err := json.Marshal(entry) + if err != nil { + continue + } + var plain map[string]json.RawMessage + if err := json.Unmarshal(entryBytes, &plain); err != nil { + continue + } + if ac, ok := plain["action_card"]; ok && actionCard == nil { + var card mcpagent.ActionCard + if err := json.Unmarshal(ac, &card); err == nil { + actionCard = &card } } + if sq, ok := plain["suggested_questions"]; ok && len(suggestedQuestions) == 0 { + _ = json.Unmarshal(sq, &suggestedQuestions) + } } out.Messages = append(out.Messages, ChatMessageOut{ ID: m.ID, @@ -223,6 +234,7 @@ func (s *Server) GetConversation(c echo.Context) error { Files: files, SuggestedQuestions: suggestedQuestions, DebugArtifacts: m.DebugArtifacts, + ActionCard: actionCard, }) } return c.JSON(http.StatusOK, out) diff --git a/internal/api/webchat_handler.go b/internal/api/webchat_handler.go index 6c873d2d0..109c0dbff 100644 --- a/internal/api/webchat_handler.go +++ b/internal/api/webchat_handler.go @@ -52,13 +52,16 @@ type WebChatChart struct { } type WebChatResponse struct { - Response string `json:"response"` - Charts []WebChatChart `json:"charts,omitempty"` - Files []WebChatFile `json:"files,omitempty"` + Response string `json:"response"` + Charts []WebChatChart `json:"charts,omitempty"` + Files []WebChatFile `json:"files,omitempty"` SuggestedQuestions []mcpagent.SuggestedQuestionCategory `json:"suggested_questions,omitempty"` - DebugArtifacts json.RawMessage `json:"debug_artifacts,omitempty"` - SessionID string `json:"sessionId,omitempty"` - ConversationID int64 `json:"conversationId"` + DebugArtifacts json.RawMessage `json:"debug_artifacts,omitempty"` + SessionID string `json:"sessionId,omitempty"` + ConversationID int64 `json:"conversationId"` + // ActionCard provides structured data for the frontend to render an action card + // (e.g. "Configuration Required") without hardcoding string matching in the UI. + ActionCard *mcpagent.ActionCard `json:"action_card,omitempty"` } // analyticsRoleFor maps permissions onto the SQL catalog's roles. Super admins @@ -197,9 +200,14 @@ func (s *Server) HandleWebChat(c echo.Context) error { mcpSession, err := mcpagent.ConnectMCP(ctx, mcpURL, mcpHeaders) if err != nil { log.Error().Err(err).Str("url", mcpURL).Msg("WebChat: failed to connect to MCP server") - errText := fmt.Sprintf("Internal AI Service Error: Unable to connect to MCP tools (%s).", err.Error()) - persistReply(errText, nil, nil, nil, "", nil) - return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": errText}) + userFriendlyErr := "> **AI Tools Unavailable**\n> \n> I am having trouble connecting to my internal data tools right now. Please try your request again in a few moments." + persistReply(userFriendlyErr, nil, nil, nil, "", nil) + resp := WebChatResponse{ + Response: userFriendlyErr, + SessionID: sessionID, + ConversationID: convID, + } + return c.JSON(http.StatusOK, resp) } if pc.CurrentOrg != nil && pc.CurrentOrg.Name != "" { @@ -221,9 +229,14 @@ func (s *Server) HandleWebChat(c echo.Context) error { responseText, updatedHistory, artifacts, debugArt, err := agent.RunTurnWithArtifacts(ctx, history, req.Message, sessionID, "livi") if err != nil { log.Error().Err(err).Msg("WebChat: agent loop failed") - errText := fmt.Sprintf("Agent loop failed: %s", err.Error()) - persistReply(errText, nil, nil, nil, "", nil) - return c.JSON(http.StatusInternalServerError, map[string]string{"error": errText}) + userFriendlyErr := "> **AI Processing Error**\n> \n> I encountered an unexpected error while processing your request. Please try asking again." + persistReply(userFriendlyErr, nil, nil, nil, "", nil) + resp := WebChatResponse{ + Response: userFriendlyErr, + SessionID: sessionID, + ConversationID: convID, + } + return c.JSON(http.StatusOK, resp) } // Whatever RunTurnWithArtifacts appended on top of the history we fed it // (possibly including a freshly swapped-in system prompt entry - see @@ -240,6 +253,21 @@ func (s *Server) HandleWebChat(c echo.Context) error { } for _, entry := range turnEntries { + if rawCard, ok := entry["action_card"]; ok && rawCard != nil && resp.ActionCard == nil { + log.Info().Interface("rawCard", rawCard).Msg("WebChat: found action_card in turnEntries") + if b, err := json.Marshal(rawCard); err == nil { + var ac mcpagent.ActionCard + if err := json.Unmarshal(b, &ac); err == nil { + acCopy := ac + resp.ActionCard = &acCopy + log.Info().Interface("actionCard", acCopy).Msg("WebChat: successfully unmarshaled action_card") + } else { + log.Error().Err(err).Msg("WebChat: failed to unmarshal action_card") + } + } else { + log.Error().Err(err).Msg("WebChat: failed to marshal rawCard") + } + } if sq, ok := entry["suggested_questions"].([]mcpagent.SuggestedQuestionCategory); ok && len(sq) > 0 { resp.SuggestedQuestions = sq break @@ -253,6 +281,10 @@ func (s *Server) HandleWebChat(c echo.Context) error { } } } + // If we have an action_card but no suggested questions were attached, add defaults. + if resp.ActionCard != nil && len(resp.SuggestedQuestions) == 0 { + resp.SuggestedQuestions = mcpagent.DefaultAIErrorSuggestedQuestions + } if vlrender.HasVegaLiteSpec(responseText) { charts, cleanText, err := extractChartsFromVega(responseText) @@ -324,6 +356,7 @@ func (s *Server) HandleWebChat(c echo.Context) error { resp.Files = append(resp.Files, chatFileFromArtifact(art, fileIDs[i])) } + log.Info().Interface("finalResp", resp).Msg("WebChat: returning response to client") return c.JSON(http.StatusOK, resp) } @@ -356,13 +389,18 @@ func titleFromFirstMessage(message string) string { // (agent.go:148-166), so replaying persisted history without it is safe, and // simpler than tracking which prompt variant was live on a given turn. func persistAssistantMessage(ctx context.Context, chatStore *storagechat.Store, convID int64, userText, assistantText string, turnEntries []mcpagent.HistoryEntry, charts []WebChatChart, artifacts []mcpagent.Artifact, rawLLMOutput string, debugArtifacts json.RawMessage) ([]int64, error) { - assistantEntries := []mcpagent.HistoryEntry{{"role": "assistant", "content": assistantText}} + assistantEntries := turnEntries + hasUser := false for i, e := range turnEntries { if role, _ := e["role"].(string); role == "user" { assistantEntries = turnEntries[i+1:] + hasUser = true break } } + if !hasUser && len(assistantEntries) == 0 { + assistantEntries = []mcpagent.HistoryEntry{{"role": "assistant", "content": assistantText}} + } chartInputs := make([]storagechat.ChartInput, 0, len(charts)) for _, ch := range charts { diff --git a/internal/docindex/docs/.synced-commits.env b/internal/docindex/docs/.synced-commits.env index b8a9278c5..9dbe34ba0 100644 --- a/internal/docindex/docs/.synced-commits.env +++ b/internal/docindex/docs/.synced-commits.env @@ -1,4 +1,4 @@ -GIT_LRC_COMMIT=1112c13df101abe2c0619ade06a652f1441a58f0 +GIT_LRC_COMMIT=be23131559d27f870c9a2a1de5a4162756d83f0d GIT_LRC_WIKI_COMMIT=a1bb17c392a46c1f3bf6cfad6396813ccf80a1a9 -HEXMOSHOMEPAGE_DOCS_COMMIT=49c80fb9bfd1a1416b3d31edc561f8ffcc0644c6 +HEXMOSHOMEPAGE_DOCS_COMMIT=7af310a4a53ad928ba813b0488a5a4f96f4ef546 LIVEREVIEW_WIKI_COMMIT=58ee4a1060bf109d889dd2cc969a776fb337d2f7 diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index 9a07b0792..bf43a677c 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/livereview/internal/aiconnectors" "github.com/livereview/internal/docindex" "github.com/livereview/internal/logging" "github.com/livereview/internal/vlrender" @@ -151,6 +152,8 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry } log.Warn().Str("failure_reason", schemaIndexFailureReason()).Msg("schema index not ready; refusing the turn before any LLM call") clog.FinalResponse(msg) + history = append(history, HistoryEntry{"role": "user", "content": userText}) + history = append(history, HistoryEntry{"role": "assistant", "content": msg, "text": msg}) return msg, history, nil, nil, nil } @@ -161,7 +164,36 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry res, err := a.classify(ctx, history, userText, clog) shape := res.Shape if err != nil { - log.Warn().Err(err).Msg("call #0 classify failed; degrading to product_guidance for this turn") + errCategory := aiconnectors.CategorizeLLMError(err) + msg := "" + var card *ActionCard + if tpl, ok := LLMErrorTemplates[errCategory]; ok { + msg = tpl.Message + card = &tpl.ActionCard + } + if msg != "" { + log.Warn().Err(err).Str("category", string(errCategory)).Msg("call #0 classify failed with provider error; short-circuiting turn") + clog.FinalResponse(msg + " (stopped after " + string(errCategory) + ")") + history = append(history, HistoryEntry{"role": "user", "content": userText}) + if card != nil { + history = append(history, HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + "suggested_questions": DefaultAIErrorSuggestedQuestions, + "action_card": card, + }) + } else { + history = append(history, HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + "suggested_questions": DefaultAIErrorSuggestedQuestions, + }) + } + return msg, history, nil, nil, nil + } + log.Warn().Err(err).Msg("call #0 classify failed with unparseable or unknown error; degrading to product_guidance for this turn") shape = shapeProductGuidance } @@ -175,8 +207,24 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry return text, history, artifacts, debugArt, err } entry := HistoryEntry{"role": "assistant", "content": text, "text": text} - if text == NoDataAnalyticsResponseText { + switch { + case text == NoDataAnalyticsResponseText: entry["suggested_questions"] = DefaultNoDataSuggestedQuestions + case strings.Contains(text, "Action Required: AI Provider Issue"): + entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions + if tpl, ok := LLMErrorTemplates[aiconnectors.ErrCategoryAuth]; ok { + entry["action_card"] = tpl.ActionCard + } + case strings.Contains(text, "AI Provider is Busy"): + entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions + if tpl, ok := LLMErrorTemplates[aiconnectors.ErrCategoryOverload]; ok { + entry["action_card"] = tpl.ActionCard + } + case strings.Contains(text, "Model No Longer Available"): + entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions + if tpl, ok := LLMErrorTemplates[aiconnectors.ErrCategoryDeprecated]; ok { + entry["action_card"] = tpl.ActionCard + } } history = append(history, entry) return text, history, artifacts, debugArt, nil @@ -297,22 +345,37 @@ func (a *Agent) runStepLoop( response, usage, err := a.provider.Complete(ctx, history, tools, completeOpts...) aiElapsed := time.Since(aiStart) if err != nil { - errCategory := CategorizeLLMError(err) + errCategory := aiconnectors.CategorizeLLMError(err) log.Error().Err(err).Int("step", step).Str("category", string(errCategory)).Msg("LLM completion failed") clog.AIError(callNumber, step, aiElapsed, err) - msg := GetLLMFallbackMessage(errCategory) + msg := "" + var card *ActionCard + if tpl, ok := LLMErrorTemplates[errCategory]; ok { + msg = tpl.Message + card = &tpl.ActionCard + } if msg == "" { return "", history, nil, nil, fmt.Errorf("llm completion step %d: %w", step, err) } clog.FinalResponse(msg + " (stopped after " + string(errCategory) + ")") - history = append(history, HistoryEntry{ - "role": "assistant", - "content": msg, - "text": msg, - "suggested_questions": DefaultAIErrorSuggestedQuestions, - }) + if card != nil { + history = append(history, HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + "suggested_questions": DefaultAIErrorSuggestedQuestions, + "action_card": card, + }) + } else { + history = append(history, HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + "suggested_questions": DefaultAIErrorSuggestedQuestions, + }) + } return msg, history, nil, nil, nil } log.Debug().Int("step", step).Int("response_len", len(response)).Msg("LLM call succeeded") @@ -591,7 +654,7 @@ func (a *Agent) interpretUserMessage(clog *logging.ChatTurnLogger, userText stri return "", err } msg := fmt.Sprintf( - "User query: %s\nOrg context: org_id = %d (%s)\n\n--- dbctx schema context ---\n%s\n\n--- available chart types ---\n%s", + "User query: %s\nOrg context: org_id = %d (%s)\n\n--- dbctx schema context ---\n%s\n\n--- available chart types ---\n%s\n\nNow, generate the JSON object containing the interpretations as requested. Do NOT output any markdown, reasoning, or text outside of the JSON object.", userText, orgID, orgName, tableText, a.chartTypes, ) return msg, nil diff --git a/internal/mcpagent/analytics.go b/internal/mcpagent/analytics.go index b4d216a3e..22cf0e5c3 100644 --- a/internal/mcpagent/analytics.go +++ b/internal/mcpagent/analytics.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/livereview/internal/aiconnectors" "github.com/livereview/internal/chatstats" "github.com/livereview/internal/livisql" "github.com/livereview/internal/logging" @@ -1569,12 +1570,27 @@ type interpretEnvelope struct { func parseInterpretations(text string) ([]Interpretation, bool) { body := strings.TrimSpace(vlrender.ExtractJSONBlock(text)) if body == "" { + log.Warn().Msg("parseInterpretations: ExtractJSONBlock returned empty string") return nil, false } var env interpretEnvelope - if err := json.Unmarshal([]byte(body), &env); err == nil && len(env.Interpretations) > 0 { - return normalizeInterpretations(env.Interpretations) + if err := json.Unmarshal([]byte(body), &env); err == nil { + log.Debug().Int("interpretations_count", len(env.Interpretations)).Msg("parseInterpretations: envelope parsed") + if len(env.Interpretations) > 0 { + // Log first entry for debugging. + first := env.Interpretations[0] + log.Debug(). + Str("name", first.Name). + Str("chart_type", first.ChartType). + Int("sql_len", len(strings.TrimSpace(first.SQL))). + Msg("parseInterpretations: first interpretation") + return normalizeInterpretations(env.Interpretations) + } + log.Warn().Str("body_prefix", truncateContent(body, 300)).Msg("parseInterpretations: envelope parsed but interpretations array is empty") + return nil, false + } else { + log.Warn().Err(err).Str("body_prefix", truncateContent(body, 300)).Msg("parseInterpretations: envelope unmarshal failed") } // Bare array fallback. @@ -1590,8 +1606,9 @@ func parseInterpretations(text string) ([]Interpretation, bool) { func normalizeInterpretations(in []Interpretation) ([]Interpretation, bool) { out := make([]Interpretation, 0, len(in)) - for _, interp := range in { + for i, interp := range in { if strings.TrimSpace(interp.SQL) == "" { + log.Warn().Int("index", i).Str("name", interp.Name).Str("chart_type", interp.ChartType).Msg("normalizeInterpretations: skipping interpretation with empty sql") continue } if strings.TrimSpace(interp.ChartType) == "" { @@ -1604,6 +1621,7 @@ func normalizeInterpretations(in []Interpretation) ([]Interpretation, bool) { if strings.TrimSpace(interp.Title) == "" { interp.Title = interp.ChartType + " chart" } + log.Debug().Int("index", i).Str("name", interp.Name).Str("chart_type", interp.ChartType).Int("sql_len", len(interp.SQL)).Msg("normalizeInterpretations: accepted") out = append(out, interp) if len(out) >= maxInterpretations { break @@ -1651,21 +1669,25 @@ func (a *Agent) runMultiInterpret( raw, err := a.completeOnce(ctx, clog, 2, "interpret", "multi", 1, system, userMsg) if err != nil { log.Error().Err(err).Msg("multi-interpret LLM call failed") - cat := CategorizeLLMError(err) - msg := GetLLMFallbackMessage(cat) + cat := aiconnectors.CategorizeLLMError(err) + msg := "" + if tpl, ok := LLMErrorTemplates[cat]; ok { + msg = tpl.Message + } if msg != "" { clog.FinalResponse(msg + " (stopped after " + string(cat) + ")") return msg, nil, debug, nil } - return "I had trouble understanding that question. Please try rephrasing.", nil, debug, nil //nolint:nilerr + log.Warn().Err(err).Str("category", string(cat)).Msg("multi-interpret LLM call failed; no known fallback") + return "I wasn't able to generate an answer this time. Please try rephrasing your question.", nil, debug, nil //nolint:nilerr } debug.LLMRawResponse = raw interps, ok := parseInterpretations(raw) if !ok { log.Warn().Str("raw_preview", truncateContent(raw, 200)).Msg("could not parse interpretations from LLM response") - return "I could not produce a valid analysis plan. Please try rephrasing.", nil, debug, nil + return "I wasn't able to generate an answer this time. Please try rephrasing your question.", nil, debug, nil } debug.Interpretations = interps diff --git a/internal/mcpagent/provider.go b/internal/mcpagent/provider.go index 8323c4208..66a88b82d 100644 --- a/internal/mcpagent/provider.go +++ b/internal/mcpagent/provider.go @@ -3,14 +3,11 @@ package mcpagent import ( "context" "encoding/json" - "errors" "fmt" - "strings" "github.com/livereview/internal/aiconnectors" "github.com/rs/zerolog/log" "github.com/tmc/langchaingo/llms" - "google.golang.org/api/googleapi" ) type Provider struct { @@ -190,99 +187,51 @@ func (p *Provider) historyToMessages(history []HistoryEntry) []llms.MessageConte return messages } -type LLMErrorCategory string - -const ( - ErrCategoryAuth LLMErrorCategory = "auth_failed" - ErrCategoryOverload LLMErrorCategory = "overloaded" - ErrCategoryTimeout LLMErrorCategory = "timeout" - ErrCategoryUnknown LLMErrorCategory = "unknown" -) - -// CategorizeLLMError maps raw SDK errors from Langchain or underlying clients -// into our standard failure categories for consistent UI fallback rendering. -func CategorizeLLMError(err error) LLMErrorCategory { - if err == nil { - return ErrCategoryUnknown - } - - // 1. Langchain-Go normalized errors - var llmsErr *llms.Error - if errors.As(err, &llmsErr) { - if llmsErr.Code == llms.ErrCodeAuthentication || llmsErr.Code == llms.ErrCodeResourceNotFound { - return ErrCategoryAuth - } - if llmsErr.Code == llms.ErrCodeRateLimit { - return ErrCategoryOverload - } - } - - // 2. Raw Google API errors (often leaked by langchaingo) - var gErr *googleapi.Error - if errors.As(err, &gErr) { - if gErr.Code == 401 || gErr.Code == 403 || gErr.Code == 404 { - return ErrCategoryAuth - } - if gErr.Code == 429 || gErr.Code == 500 || gErr.Code == 502 || gErr.Code == 503 || gErr.Code == 504 { - return ErrCategoryOverload - } - } - - // 3. Generic HTTP status codes - var scErr interface{ StatusCode() int } - if errors.As(err, &scErr) { - code := scErr.StatusCode() - if code == 401 || code == 403 || code == 404 { - return ErrCategoryAuth - } - if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { - return ErrCategoryOverload - } - } - - var hscErr interface{ HTTPStatusCode() int } - if errors.As(err, &hscErr) { - code := hscErr.HTTPStatusCode() - if code == 401 || code == 403 || code == 404 { - return ErrCategoryAuth - } - if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { - return ErrCategoryOverload - } - } - - // 4. Fallback string matching - msg := strings.ToLower(err.Error()) - if strings.Contains(msg, "context deadline") || strings.Contains(msg, "timeout") { - return ErrCategoryTimeout - } - if strings.Contains(msg, "rate limit") || strings.Contains(msg, "too many requests") || - strings.Contains(msg, "status code: 429") || strings.Contains(msg, "status code: 50") || - strings.Contains(msg, "service unavailable") || strings.Contains(msg, "bad gateway") || - strings.Contains(msg, "high demand") { - return ErrCategoryOverload - } - if strings.Contains(msg, "deprecated") || strings.Contains(msg, "model not found") || - strings.Contains(msg, "status code: 401") || strings.Contains(msg, "status code: 403") || - strings.Contains(msg, "status code: 404") || strings.Contains(msg, "unauthorized") || - strings.Contains(msg, "forbidden") { - return ErrCategoryAuth - } - - return ErrCategoryUnknown +// LLMErrorTemplate defines the UI representation of an LLM error, +// including both the chat message text and the Action Card to render. +type LLMErrorTemplate struct { + Message string + ActionCard ActionCard } -// GetLLMFallbackMessage returns a safe, beautiful UI response for the client -// based on the categorized LLM error. Returns empty string for unknown errors. -func GetLLMFallbackMessage(cat LLMErrorCategory) string { - switch cat { - case ErrCategoryAuth: - return "> **Action Required: AI Provider Issue**\n> \n> The AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.\n> \n> [btn:Configure AI Provider](/settings#ai)" - case ErrCategoryOverload: - return "> **AI Provider is Busy**\n> \n> The AI provider is currently experiencing high traffic. If you continue to see this issue, we suggest changing to a different model or provider." - case ErrCategoryTimeout: - return "> **Analysis Took Too Long**\n> \n> The AI took too long to generate your response and timed out. Try asking a narrower or more specific question." - default: - return "" - } +// LLMErrorTemplates stores the fallback configurations for various LLM errors. +// This allows the frontend to automatically render the correct box without +// hardcoded display logic. +var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ + aiconnectors.ErrCategoryAuth: { + Message: "**Action Required: AI Provider Issue**\n\nThe AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.", + ActionCard: ActionCard{ + Title: "Configuration Required", + Description: "Please edit the current AI provider configuration to continue.", + ButtonText: "Configure AI Provider", + ActionURL: "/ai", + }, + }, + aiconnectors.ErrCategoryOverload: { + Message: "**AI Provider is Busy**\n\nThe selected model is currently experiencing high traffic. If you continue to see this issue, please edit your configuration to change to a different model or provider.", + ActionCard: ActionCard{ + Title: "High Traffic Detected", + Description: "Please select a different model in settings.", + ButtonText: "Configure AI Provider", + ActionURL: "/ai", + }, + }, + aiconnectors.ErrCategoryDeprecated: { + Message: "**Model No Longer Available**\n\nThe selected AI model has been deprecated or removed by the provider. Please update your AI provider configuration to use a different model.", + ActionCard: ActionCard{ + Title: "Model No Longer Available", + Description: "The selected model has been deprecated. Please update your AI provider configuration.", + ButtonText: "Configure AI Provider", + ActionURL: "/ai", + }, + }, + aiconnectors.ErrCategoryTimeout: { + Message: "**Analysis Took Too Long**\n\nThe AI took too long to generate your response and timed out. Try asking a narrower or more specific question.", + ActionCard: ActionCard{ + Title: "Timeout Error", + Description: "The request took too long to complete.", + ButtonText: "Try Again", + ActionURL: "/chat", + }, + }, } diff --git a/internal/mcpagent/provider_test.go b/internal/mcpagent/provider_test.go new file mode 100644 index 000000000..d9aac34b7 --- /dev/null +++ b/internal/mcpagent/provider_test.go @@ -0,0 +1,53 @@ +package mcpagent + +import ( + "testing" + + "github.com/livereview/internal/aiconnectors" +) + +func TestLLMErrorTemplates(t *testing.T) { + tests := []struct { + name string + category aiconnectors.LLMErrorCategory + want string + }{ + { + name: "Auth Error", + category: aiconnectors.ErrCategoryAuth, + want: "**Action Required: AI Provider Issue**\n\nThe AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.", + }, + { + name: "Overload Error", + category: aiconnectors.ErrCategoryOverload, + want: "**AI Provider is Busy**\n\nThe selected model is currently experiencing high traffic. If you continue to see this issue, please edit your configuration to change to a different model or provider.", + }, + { + name: "Timeout Error", + category: aiconnectors.ErrCategoryTimeout, + want: "**Analysis Took Too Long**\n\nThe AI took too long to generate your response and timed out. Try asking a narrower or more specific question.", + }, + { + name: "Unknown Error", + category: aiconnectors.ErrCategoryUnknown, + want: "", + }, + { + name: "Unmapped Category", + category: aiconnectors.LLMErrorCategory("something_else"), + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := "" + if tpl, ok := LLMErrorTemplates[tt.category]; ok { + got = tpl.Message + } + if got != tt.want { + t.Errorf("LLMErrorTemplates[%v].Message = %q, want %q", tt.category, got, tt.want) + } + }) + } +} diff --git a/internal/mcpagent/types.go b/internal/mcpagent/types.go index e07130a39..e5f39b061 100644 --- a/internal/mcpagent/types.go +++ b/internal/mcpagent/types.go @@ -134,3 +134,12 @@ type DebugResultEntry struct { RetryCount int `json:"retry_count,omitempty"` Retries []RetryInfo `json:"retries,omitempty"` } + +// ActionCard represents an actionable message for the UI to render as a card +// instead of plain text, like when a configuration change is required. +type ActionCard struct { + Title string `json:"title"` + Description string `json:"description"` + ButtonText string `json:"button_text"` + ActionURL string `json:"action_url"` +} diff --git a/ui/src/api/chatConversations.ts b/ui/src/api/chatConversations.ts index 1cadb9f21..7f2f8c568 100644 --- a/ui/src/api/chatConversations.ts +++ b/ui/src/api/chatConversations.ts @@ -27,6 +27,12 @@ export interface ConversationMessage { files?: ChatFile[]; suggested_questions?: SuggestedQuestionCategory[]; debug_artifacts?: unknown; + action_card?: { + title: string; + description: string; + button_text: string; + action_url: string; + }; } export interface ConversationDetail { diff --git a/ui/src/api/chatbot.ts b/ui/src/api/chatbot.ts index f081e0712..1deb4193c 100644 --- a/ui/src/api/chatbot.ts +++ b/ui/src/api/chatbot.ts @@ -142,6 +142,13 @@ export interface ChatResponse { debug_artifacts?: unknown; sessionId?: string; conversationId: number; + /** 'ai_auth' | 'ai_busy' — set only when the backend hit a provider error. */ + action_card?: { + title: string; + description: string; + button_text: string; + action_url: string; + }; } // The backend now owns conversation history: it loads prior turns by diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index 13e29e619..f07761b21 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -117,6 +117,12 @@ interface ChatEntry { files?: ChatFile[]; suggestedQuestions?: SuggestedQuestionCategory[]; debugArtifacts?: DebugArtifacts | null; + actionCard?: { + title: string; + description: string; + button_text: string; + action_url: string; + }; } function formatRowCount(rows?: number): string { @@ -891,6 +897,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } files: m.files && m.files.length > 0 ? m.files : undefined, suggestedQuestions: m.suggested_questions, debugArtifacts: m.debug_artifacts as DebugArtifacts | undefined, + actionCard: m.action_card, })), ); }, [conversationDetail]); @@ -933,6 +940,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } } const result = await sendChatMessage(text, activeConversationId); + console.log("LIVE API RESPONSE:", result); const assistantEntry: ChatEntry = { id: generateId(), @@ -942,6 +950,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } files: result.files && result.files.length > 0 ? result.files : undefined, suggestedQuestions: result.suggested_questions, debugArtifacts: result.debug_artifacts as DebugArtifacts | undefined, + actionCard: result.action_card, }; setMessages((prev) => [...prev, assistantEntry]); queryClient.invalidateQueries({ queryKey: CONVERSATIONS_QUERY_KEY }); @@ -1082,8 +1091,8 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } const debugModalMsg = debugModalMsgId ? messages.find((m) => m.id === debugModalMsgId) : undefined; return ( -
-
+
+
@@ -1142,7 +1151,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
-
+
{isSuperAdmin && !dismissedProdUrlWarning && ( setDismissedProdUrlWarning(true)} /> @@ -1472,34 +1481,25 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } ))}
)} - {msg.text && (() => { - const isAiError = msg.text.includes('Action Required: AI Provider Issue'); - const displayText = isAiError && msg.text.includes('Please configure a valid provider to continue:') - ? msg.text.split('Please configure a valid provider to continue:')[0].trim() - : msg.text; - - return ( - <> -
0) || (msg.files && msg.files.length > 0) ? 'mt-6' : ''} text-base leading-snug whitespace-pre-wrap break-words text-slate-200 [&>*:first-child]:mt-0`}> - {formatText(displayText)} -
- {isAiError && ( -
-
-

Configuration Required

-

Please edit the current AI provider model to continue.

-
- -
- )} - - ); - })()} + {msg.text && ( +
0) || (msg.files && msg.files.length > 0) ? 'mt-6' : ''} text-base leading-snug whitespace-pre-wrap break-words text-slate-200 [&>*:first-child]:mt-0`}> + {formatText(msg.text)} +
+ )} + {msg.actionCard && ( +
+
+

{msg.actionCard.title}

+

{msg.actionCard.description}

+
+ +
+ )} {msg.suggestedQuestions && msg.suggestedQuestions.length > 0 && (
{msg.suggestedQuestions.map((cat: SuggestedQuestionCategory, catIdx: number) => ( @@ -1546,7 +1546,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
-
+
Date: Sat, 3 Oct 2026 15:30:05 +0530 Subject: [PATCH 03/24] Clean Up WebChat Logging and Refactor Chat UI Layout --- internal/api/webchat_handler.go | 2 +- ui/src/pages/Chatbot/ChatConversation.tsx | 15 ++++++++------- ui/src/pages/Chatbot/ChatLayout.tsx | 12 +++++++++++- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/internal/api/webchat_handler.go b/internal/api/webchat_handler.go index 109c0dbff..8978cb52d 100644 --- a/internal/api/webchat_handler.go +++ b/internal/api/webchat_handler.go @@ -356,7 +356,7 @@ func (s *Server) HandleWebChat(c echo.Context) error { resp.Files = append(resp.Files, chatFileFromArtifact(art, fileIDs[i])) } - log.Info().Interface("finalResp", resp).Msg("WebChat: returning response to client") + // log.Info().Interface("finalResp", resp).Msg("WebChat: returning response to client") return c.JSON(http.StatusOK, resp) } diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index f07761b21..719316290 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -68,9 +68,7 @@ const ContextDetails: React.FC<{ context: ChartContext }> = ({ context }) => ( // Unified logo wrapper to ensure the 16px visual right gap and 8px visual left gap // stay mathematically synchronized across the header, chat messages, and loading states. const LiviLogo: React.FC<{ className?: string }> = ({ className = '' }) => ( -
- Bot -
+ Bot ); // Debug artifacts (SQL, CSV, Vega spec, schema context, system prompt, raw @@ -1091,8 +1089,11 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } const debugModalMsg = debugModalMsgId ? messages.find((m) => m.id === debugModalMsgId) : undefined; return ( -
-
+
+ {/* Header: pl-4 pr-6 instead of px-4 so max-w-4xl mx-auto centers identically + to the messages section which has scrollbar-gutter:stable (8px) on the right. + Math: header right=24px, messages right=16px(px-4)+8px(gutter)=24px → same. */} +
@@ -1151,7 +1152,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
-
+
{isSuperAdmin && !dismissedProdUrlWarning && ( setDismissedProdUrlWarning(true)} /> @@ -1546,7 +1547,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
-
+
{ const reservedWidth = useChatSidebarReservedWidth(); const resizing = useChatSidebarResizing(); + useEffect(() => { + // Disable body scrolling on the chat layout to prevent a double scrollbar + // caused by subpixel layout overflows or banners pushing the layout down. + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, []); + return (
From 8b9b2b487f71140a0ef9388c3c1332a83ef723d4 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 3 Oct 2026 15:41:39 +0530 Subject: [PATCH 04/24] Add Raw LLM Error Tracking to Debug Artifacts --- internal/api/webchat_handler.go | 5 ++++ internal/mcpagent/agent.go | 4 ++- internal/mcpagent/analytics.go | 1 + internal/mcpagent/provider.go | 8 +++--- internal/mcpagent/types.go | 1 + ui/src/pages/Chatbot/ChatConversation.tsx | 33 ++++++++++++++++------- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/internal/api/webchat_handler.go b/internal/api/webchat_handler.go index 8978cb52d..f9ebcef86 100644 --- a/internal/api/webchat_handler.go +++ b/internal/api/webchat_handler.go @@ -268,6 +268,11 @@ func (s *Server) HandleWebChat(c echo.Context) error { log.Error().Err(err).Msg("WebChat: failed to marshal rawCard") } } + if rawDebug, ok := entry["debug_artifacts"]; ok && rawDebug != nil && resp.DebugArtifacts == nil { + if b, err := json.Marshal(rawDebug); err == nil { + resp.DebugArtifacts = b + } + } if sq, ok := entry["suggested_questions"].([]mcpagent.SuggestedQuestionCategory); ok && len(sq) > 0 { resp.SuggestedQuestions = sq break diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index bf43a677c..ab24b862a 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -182,6 +182,7 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry "text": msg, "suggested_questions": DefaultAIErrorSuggestedQuestions, "action_card": card, + "debug_artifacts": &DebugArtifacts{RawLLMError: err.Error()}, }) } else { history = append(history, HistoryEntry{ @@ -191,7 +192,7 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry "suggested_questions": DefaultAIErrorSuggestedQuestions, }) } - return msg, history, nil, nil, nil + return msg, history, nil, &DebugArtifacts{RawLLMError: err.Error()}, nil } log.Warn().Err(err).Msg("call #0 classify failed with unparseable or unknown error; degrading to product_guidance for this turn") shape = shapeProductGuidance @@ -367,6 +368,7 @@ func (a *Agent) runStepLoop( "text": msg, "suggested_questions": DefaultAIErrorSuggestedQuestions, "action_card": card, + "debug_artifacts": &DebugArtifacts{RawLLMError: err.Error()}, }) } else { history = append(history, HistoryEntry{ diff --git a/internal/mcpagent/analytics.go b/internal/mcpagent/analytics.go index 22cf0e5c3..001f5e069 100644 --- a/internal/mcpagent/analytics.go +++ b/internal/mcpagent/analytics.go @@ -1668,6 +1668,7 @@ func (a *Agent) runMultiInterpret( debug.FullRequest = system + "\n---\n" + userMsg raw, err := a.completeOnce(ctx, clog, 2, "interpret", "multi", 1, system, userMsg) if err != nil { + debug.RawLLMError = err.Error() log.Error().Err(err).Msg("multi-interpret LLM call failed") cat := aiconnectors.CategorizeLLMError(err) msg := "" diff --git a/internal/mcpagent/provider.go b/internal/mcpagent/provider.go index 66a88b82d..850c07584 100644 --- a/internal/mcpagent/provider.go +++ b/internal/mcpagent/provider.go @@ -199,7 +199,7 @@ type LLMErrorTemplate struct { // hardcoded display logic. var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ aiconnectors.ErrCategoryAuth: { - Message: "**Action Required: AI Provider Issue**\n\nThe AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.", + Message: "> **Action Required: AI Provider Issue**\n> \n> The AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.", ActionCard: ActionCard{ Title: "Configuration Required", Description: "Please edit the current AI provider configuration to continue.", @@ -208,7 +208,7 @@ var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ }, }, aiconnectors.ErrCategoryOverload: { - Message: "**AI Provider is Busy**\n\nThe selected model is currently experiencing high traffic. If you continue to see this issue, please edit your configuration to change to a different model or provider.", + Message: "> **AI Provider is Busy**\n> \n> The selected model is currently experiencing high traffic. If you continue to see this issue, please edit your configuration to change to a different model or provider.", ActionCard: ActionCard{ Title: "High Traffic Detected", Description: "Please select a different model in settings.", @@ -217,7 +217,7 @@ var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ }, }, aiconnectors.ErrCategoryDeprecated: { - Message: "**Model No Longer Available**\n\nThe selected AI model has been deprecated or removed by the provider. Please update your AI provider configuration to use a different model.", + Message: "> **Model No Longer Available**\n> \n> The selected AI model has been deprecated or removed by the provider. Please update your AI provider configuration to use a different model.", ActionCard: ActionCard{ Title: "Model No Longer Available", Description: "The selected model has been deprecated. Please update your AI provider configuration.", @@ -226,7 +226,7 @@ var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ }, }, aiconnectors.ErrCategoryTimeout: { - Message: "**Analysis Took Too Long**\n\nThe AI took too long to generate your response and timed out. Try asking a narrower or more specific question.", + Message: "> **Analysis Took Too Long**\n> \n> The AI took too long to generate your response and timed out. Try asking a narrower or more specific question.", ActionCard: ActionCard{ Title: "Timeout Error", Description: "The request took too long to complete.", diff --git a/internal/mcpagent/types.go b/internal/mcpagent/types.go index e5f39b061..77e2391f6 100644 --- a/internal/mcpagent/types.go +++ b/internal/mcpagent/types.go @@ -117,6 +117,7 @@ type DebugArtifacts struct { FullRequest string `json:"full_request"` // system_prompt + schema_context sent to LLM Interpretations []Interpretation `json:"interpretations"` Results []DebugResultEntry `json:"results"` + RawLLMError string `json:"raw_llm_error,omitempty"` } // DebugResultEntry is one interpretation's outcome for the debug view. diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index 719316290..b2e669e1e 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -105,6 +105,7 @@ interface DebugArtifacts { repaired_sql?: string; }>; }>; + raw_llm_error?: string; } interface ChatEntry { @@ -1488,17 +1489,29 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
)} {msg.actionCard && ( -
-
-

{msg.actionCard.title}

-

{msg.actionCard.description}

+
+
+
+

{msg.actionCard.title}

+

{msg.actionCard.description}

+
+
- + {msg.debugArtifacts?.raw_llm_error && ( +
+ + Debug logs + +
+                                {msg.debugArtifacts.raw_llm_error}
+                              
+
+ )}
)} {msg.suggestedQuestions && msg.suggestedQuestions.length > 0 && ( From e2a887165c02465a59bbbb8a79cd447febb91c0b Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 3 Oct 2026 15:51:00 +0530 Subject: [PATCH 05/24] Update Error Identification and Layout in Chat Conversation --- ui/src/pages/Chatbot/ChatConversation.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index b2e669e1e..d2beba865 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -222,22 +222,29 @@ function formatText(rawText: string): React.ReactNode[] { } i = currI - 1; - const isError = blockquoteLines.length > 0 && blockquoteLines[0].includes('Action Required:'); + const errorPhrases = [ + 'Action Required:', + 'AI Provider is Busy', + 'Model No Longer Available', + 'Analysis Took Too Long', + 'AI Tools Unavailable' + ]; + const isError = blockquoteLines.length > 0 && errorPhrases.some(phrase => blockquoteLines[0].includes(phrase)); if (isError) { parts.push( -
-
+
+
{formatLine(blockquoteLines[0])}
-
+
{blockquoteLines.slice(1).map((bLine, bIdx) => (
*:first-child]:mt-0'}> {formatLine(bLine)}
))}
-
+
); } else { parts.push( @@ -1222,7 +1229,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
) : (
- +
{msg.charts && msg.charts.length > 0 && (
@@ -1550,7 +1557,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } {isLoading && (
- +
From 78cf87d3efe5ed00c395ae714be7feed8cbc2701 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 3 Oct 2026 17:44:06 +0530 Subject: [PATCH 06/24] Refactor LLM Classification Error Handling --- internal/mcpagent/agent.go | 67 +++++++++----------------------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index ab24b862a..ca01c1981 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -164,37 +164,7 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry res, err := a.classify(ctx, history, userText, clog) shape := res.Shape if err != nil { - errCategory := aiconnectors.CategorizeLLMError(err) - msg := "" - var card *ActionCard - if tpl, ok := LLMErrorTemplates[errCategory]; ok { - msg = tpl.Message - card = &tpl.ActionCard - } - if msg != "" { - log.Warn().Err(err).Str("category", string(errCategory)).Msg("call #0 classify failed with provider error; short-circuiting turn") - clog.FinalResponse(msg + " (stopped after " + string(errCategory) + ")") - history = append(history, HistoryEntry{"role": "user", "content": userText}) - if card != nil { - history = append(history, HistoryEntry{ - "role": "assistant", - "content": msg, - "text": msg, - "suggested_questions": DefaultAIErrorSuggestedQuestions, - "action_card": card, - "debug_artifacts": &DebugArtifacts{RawLLMError: err.Error()}, - }) - } else { - history = append(history, HistoryEntry{ - "role": "assistant", - "content": msg, - "text": msg, - "suggested_questions": DefaultAIErrorSuggestedQuestions, - }) - } - return msg, history, nil, &DebugArtifacts{RawLLMError: err.Error()}, nil - } - log.Warn().Err(err).Msg("call #0 classify failed with unparseable or unknown error; degrading to product_guidance for this turn") + log.Warn().Err(err).Str("category", string(aiconnectors.CategorizeLLMError(err))).Msg("call #0 classify failed; degrading to product_guidance for this turn") shape = shapeProductGuidance } @@ -227,6 +197,7 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry entry["action_card"] = tpl.ActionCard } } + history = append(history, entry) return text, history, artifacts, debugArt, nil } @@ -287,7 +258,8 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry } clog.BranchSelected(string(shape), len(systemPrompt), len(tools)) - return a.runStepLoop(ctx, history, userText, systemPrompt, tools, callNumber, jsonMode, planRetried, "", clog) + text, history, artifacts, debugArt, err := a.runStepLoop(ctx, history, userText, systemPrompt, tools, callNumber, jsonMode, planRetried, "", clog) + return text, history, artifacts, debugArt, err } // Non-analytics path (plain tool-only agent). @@ -342,7 +314,7 @@ func (a *Agent) runStepLoop( if jsonMode { completeOpts = append(completeOpts, llms.WithJSONMode()) } - + response, usage, err := a.provider.Complete(ctx, history, tools, completeOpts...) aiElapsed := time.Since(aiStart) if err != nil { @@ -361,23 +333,18 @@ func (a *Agent) runStepLoop( } clog.FinalResponse(msg + " (stopped after " + string(errCategory) + ")") + entry := HistoryEntry{ + "role": "assistant", + "content": msg, + "text": msg, + "suggested_questions": DefaultAIErrorSuggestedQuestions, + } if card != nil { - history = append(history, HistoryEntry{ - "role": "assistant", - "content": msg, - "text": msg, - "suggested_questions": DefaultAIErrorSuggestedQuestions, - "action_card": card, - "debug_artifacts": &DebugArtifacts{RawLLMError: err.Error()}, - }) - } else { - history = append(history, HistoryEntry{ - "role": "assistant", - "content": msg, - "text": msg, - "suggested_questions": DefaultAIErrorSuggestedQuestions, - }) + entry["action_card"] = card + entry["debug_artifacts"] = &DebugArtifacts{RawLLMError: err.Error()} } + + history = append(history, entry) return msg, history, nil, nil, nil } log.Debug().Int("step", step).Int("response_len", len(response)).Msg("LLM call succeeded") @@ -662,10 +629,6 @@ func (a *Agent) interpretUserMessage(clog *logging.ChatTurnLogger, userText stri return msg, nil } - - - - // isAuthError reports whether a tool result signals an expired/invalid // session token or missing auth - the exact messages LiveReview's own auth // middleware returns (internal/api/auth/middleware.go). Retrying with a From 8642a73a9c5323ceab204e68c772abaff2dd9c7b Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 3 Oct 2026 18:35:06 +0530 Subject: [PATCH 07/24] Update LLM Error Handling and UI Styles --- internal/aiconnectors/errors.go | 12 ++++--- internal/mcpagent/agent.go | 40 ++++++++++++++--------- internal/mcpagent/analytics.go | 3 +- ui/src/pages/Chatbot/ChatConversation.tsx | 6 ++-- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/internal/aiconnectors/errors.go b/internal/aiconnectors/errors.go index 7c15883e1..c31475fe7 100644 --- a/internal/aiconnectors/errors.go +++ b/internal/aiconnectors/errors.go @@ -24,6 +24,8 @@ func CategorizeLLMError(err error) LLMErrorCategory { return ErrCategoryUnknown } + + // 1. Langchain-Go normalized errors var llmsErr *llms.Error if errors.As(err, &llmsErr) { @@ -60,6 +62,12 @@ func CategorizeLLMError(err error) LLMErrorCategory { // 3. Fallback string matching msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "deprecated") || strings.Contains(msg, "model not found") || + strings.Contains(msg, "model_not_found") || strings.Contains(msg, "no such model") || + strings.Contains(msg, "no longer available") { + return ErrCategoryDeprecated + } + if strings.Contains(msg, "context deadline") || strings.Contains(msg, "timeout") { return ErrCategoryTimeout } @@ -70,10 +78,6 @@ func CategorizeLLMError(err error) LLMErrorCategory { strings.Contains(msg, "high demand") { return ErrCategoryOverload } - if strings.Contains(msg, "deprecated") || strings.Contains(msg, "model not found") || - strings.Contains(msg, "model_not_found") || strings.Contains(msg, "no such model") { - return ErrCategoryDeprecated - } if strings.Contains(msg, "status code: 401") || strings.Contains(msg, "status code: 403") || strings.Contains(msg, "status code: 404") || strings.Contains(msg, "unauthorized") || strings.Contains(msg, "error 401") || strings.Contains(msg, "error 403") || diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index ca01c1981..71e32cf38 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -178,25 +178,31 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry return text, history, artifacts, debugArt, err } entry := HistoryEntry{"role": "assistant", "content": text, "text": text} - switch { - case text == NoDataAnalyticsResponseText: + if text == NoDataAnalyticsResponseText { entry["suggested_questions"] = DefaultNoDataSuggestedQuestions - case strings.Contains(text, "Action Required: AI Provider Issue"): - entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions - if tpl, ok := LLMErrorTemplates[aiconnectors.ErrCategoryAuth]; ok { - entry["action_card"] = tpl.ActionCard - } - case strings.Contains(text, "AI Provider is Busy"): - entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions - if tpl, ok := LLMErrorTemplates[aiconnectors.ErrCategoryOverload]; ok { - entry["action_card"] = tpl.ActionCard + } else { + for _, tpl := range LLMErrorTemplates { + if strings.HasPrefix(text, tpl.Message) { + entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions + card := tpl.ActionCard // copy + entry["action_card"] = &card + entry["debug_artifacts"] = debugArt + break } - case strings.Contains(text, "Model No Longer Available"): + } + + // Also check for format errors, which are hallucinations, not provider errors + if strings.Contains(text, "Model Output Error") { entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions - if tpl, ok := LLMErrorTemplates[aiconnectors.ErrCategoryDeprecated]; ok { - entry["action_card"] = tpl.ActionCard + entry["action_card"] = &ActionCard{ + Title: "Formatting Error", + Description: "The model failed to format the response correctly.", + ButtonText: "Try Again", + ActionURL: "/chat", } + entry["debug_artifacts"] = debugArt } + } // close else block history = append(history, entry) return text, history, artifacts, debugArt, nil @@ -339,13 +345,15 @@ func (a *Agent) runStepLoop( "text": msg, "suggested_questions": DefaultAIErrorSuggestedQuestions, } + var debugArt *DebugArtifacts if card != nil { entry["action_card"] = card - entry["debug_artifacts"] = &DebugArtifacts{RawLLMError: err.Error()} + debugArt = &DebugArtifacts{RawLLMError: err.Error()} + entry["debug_artifacts"] = debugArt } history = append(history, entry) - return msg, history, nil, nil, nil + return msg, history, nil, debugArt, nil } log.Debug().Int("step", step).Int("response_len", len(response)).Msg("LLM call succeeded") clog.AIResponse(callNumber, step, aiElapsed, usage.InputTokens, usage.OutputTokens, response) diff --git a/internal/mcpagent/analytics.go b/internal/mcpagent/analytics.go index 001f5e069..d47786974 100644 --- a/internal/mcpagent/analytics.go +++ b/internal/mcpagent/analytics.go @@ -1688,7 +1688,8 @@ func (a *Agent) runMultiInterpret( interps, ok := parseInterpretations(raw) if !ok { log.Warn().Str("raw_preview", truncateContent(raw, 200)).Msg("could not parse interpretations from LLM response") - return "I wasn't able to generate an answer this time. Please try rephrasing your question.", nil, debug, nil + formatMsg := "> **Model Output Error**\n> \n> The AI returned data in an unexpected format. Please try rephrasing your question." + return formatMsg, nil, debug, nil } debug.Interpretations = interps diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index d2beba865..fc12bfac8 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -1418,7 +1418,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } )} {(chart.query || chart.time_range || chart.granularity || chart.context) && (
- + Data details
@@ -1469,7 +1469,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } )} {(file.query || file.time_range || file.granularity || file.context) && (
- + Data details
@@ -1511,7 +1511,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
{msg.debugArtifacts?.raw_llm_error && (
- + Debug logs

From b91a11155b54040f990e714be5d1790b8459e79e Mon Sep 17 00:00:00 2001
From: Ganesh Kumar 
Date: Sat, 3 Oct 2026 18:54:29 +0530
Subject: [PATCH 08/24] Refactor Error Handling and Timeouts in AI and Chat
 Services

---
 internal/aiconnectors/errors.go           | 7 +++++--
 internal/api/webchat_handler.go           | 2 +-
 internal/mcpagent/analytics.go            | 2 +-
 internal/mcpagent/provider.go             | 9 +++++++++
 ui/src/pages/Chatbot/ChatConversation.tsx | 2 +-
 5 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/internal/aiconnectors/errors.go b/internal/aiconnectors/errors.go
index c31475fe7..7223b3ebc 100644
--- a/internal/aiconnectors/errors.go
+++ b/internal/aiconnectors/errors.go
@@ -72,8 +72,11 @@ func CategorizeLLMError(err error) LLMErrorCategory {
 		return ErrCategoryTimeout
 	}
 	if strings.Contains(msg, "rate limit") || strings.Contains(msg, "too many requests") ||
-		strings.Contains(msg, "status code: 429") || strings.Contains(msg, "status code: 50") ||
-		strings.Contains(msg, "error 429") || strings.Contains(msg, "error 50") ||
+		strings.Contains(msg, "status code: 429") || strings.Contains(msg, "status code: 500") ||
+		strings.Contains(msg, "status code: 502") || strings.Contains(msg, "status code: 503") ||
+		strings.Contains(msg, "status code: 504") || strings.Contains(msg, "error 429") ||
+		strings.Contains(msg, "error 500") || strings.Contains(msg, "error 502") ||
+		strings.Contains(msg, "error 503") || strings.Contains(msg, "error 504") ||
 		strings.Contains(msg, "service unavailable") || strings.Contains(msg, "bad gateway") ||
 		strings.Contains(msg, "high demand") {
 		return ErrCategoryOverload
diff --git a/internal/api/webchat_handler.go b/internal/api/webchat_handler.go
index f9ebcef86..009850061 100644
--- a/internal/api/webchat_handler.go
+++ b/internal/api/webchat_handler.go
@@ -207,7 +207,7 @@ func (s *Server) HandleWebChat(c echo.Context) error {
 			SessionID:      sessionID,
 			ConversationID: convID,
 		}
-		return c.JSON(http.StatusOK, resp)
+		return c.JSON(http.StatusBadGateway, resp)
 	}
 
 	if pc.CurrentOrg != nil && pc.CurrentOrg.Name != "" {
diff --git a/internal/mcpagent/analytics.go b/internal/mcpagent/analytics.go
index d47786974..c564486c2 100644
--- a/internal/mcpagent/analytics.go
+++ b/internal/mcpagent/analytics.go
@@ -43,7 +43,7 @@ const (
 
 	// analyticsTurnTimeout bounds the whole fan-out regardless of per-query
 	// timeouts, so a slow model cannot hold a request open indefinitely.
-	analyticsTurnTimeout = 300 * time.Second
+	analyticsTurnTimeout = 90 * time.Second
 )
 
 // AnalyticsEngine executes guard-rewritten SQL. Declared here rather than
diff --git a/internal/mcpagent/provider.go b/internal/mcpagent/provider.go
index 850c07584..4fca75443 100644
--- a/internal/mcpagent/provider.go
+++ b/internal/mcpagent/provider.go
@@ -234,4 +234,13 @@ var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{
 			ActionURL:   "/chat",
 		},
 	},
+	aiconnectors.ErrCategoryUnknown: {
+		Message: "> **Unexpected Error**\n> \n> An unexpected error occurred while communicating with the AI provider. Please check the debug logs for more details.",
+		ActionCard: ActionCard{
+			Title:       "Unexpected Error",
+			Description: "An unknown error occurred during processing.",
+			ButtonText:  "Try Again",
+			ActionURL:   "/chat",
+		},
+	},
 }
diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx
index fc12bfac8..8833ed8eb 100644
--- a/ui/src/pages/Chatbot/ChatConversation.tsx
+++ b/ui/src/pages/Chatbot/ChatConversation.tsx
@@ -1503,7 +1503,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
                               

{msg.actionCard.description}

+
+ {msg.debugArtifacts?.raw_llm_error && ( +
+ + Debug logs + +
+ {msg.debugArtifacts.raw_llm_error} +
+
+ )} +
+ ); +} diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index 484717bab..197fc5a89 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -13,6 +13,8 @@ import { CONVERSATIONS_QUERY_KEY } from './ConversationSidebar'; import { isDailyTrendChart, buildTrendSpec, formatAxisDate, Granularity } from './rebucketChart'; import { buildJsonExport, downloadJsonExport } from './jsonExport'; import ProductionUrlWarning from '../../components/ProductionUrlWarning'; +import { ChatActionCard } from './ChatActionCard'; +import { ChatErrorBlockquote } from './ChatErrorHandling'; const GRANULARITY_OPTIONS: { value: Granularity; label: string }[] = [ { value: 'day', label: 'Day' }, @@ -108,7 +110,7 @@ interface DebugArtifacts { raw_llm_error?: string; } -interface ChatEntry { +export interface ChatEntry { id: string; role: 'user' | 'assistant'; text: string; @@ -116,6 +118,7 @@ interface ChatEntry { files?: ChatFile[]; suggestedQuestions?: SuggestedQuestionCategory[]; debugArtifacts?: DebugArtifacts | null; + isError?: boolean; actionCard?: { title: string; description: string; @@ -174,7 +177,7 @@ function extractTrailingDataDetails(text: string): { body: string; details: { la return { body: lines.slice(0, end).join('\n'), details }; } -function formatText(rawText: string): React.ReactNode[] { +function formatText(rawText: string, isErrorMsg: boolean = false): React.ReactNode[] { const { body: text, details } = extractTrailingDataDetails(rawText); const parts: React.ReactNode[] = []; // Ensure buttons are always inline with the preceding text by stripping newlines before them @@ -222,29 +225,13 @@ function formatText(rawText: string): React.ReactNode[] { } i = currI - 1; - const errorPhrases = [ - 'Action Required:', - 'AI Provider is Busy', - 'Model No Longer Available', - 'Analysis Took Too Long', - 'AI Tools Unavailable' - ]; - const isError = blockquoteLines.length > 0 && errorPhrases.some(phrase => blockquoteLines[0].includes(phrase)); - - if (isError) { + if (isErrorMsg) { parts.push( -
-
- {formatLine(blockquoteLines[0])} -
-
- {blockquoteLines.slice(1).map((bLine, bIdx) => ( -
*:first-child]:mt-0'}> - {formatLine(bLine)} -
- ))} -
-
+ ); } else { parts.push( @@ -903,6 +890,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } files: m.files && m.files.length > 0 ? m.files : undefined, suggestedQuestions: m.suggested_questions, debugArtifacts: m.debug_artifacts as DebugArtifacts | undefined, + isError: m.is_error, actionCard: m.action_card, })), ); @@ -924,10 +912,13 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } pendingConversationIdRef.current = undefined; }, [conversationId]); - const handleSend = async () => { - const text = input.trim(); + const handleSend = async (overrideText?: string | React.SyntheticEvent) => { + const isString = typeof overrideText === 'string'; + const text = (isString ? overrideText : input).trim(); if (!text || isLoading) return; - setInput(''); + if (!isString) { + setInput(''); + } const userEntry: ChatEntry = { id: generateId(), role: 'user', text }; setMessages((prev) => [...prev, userEntry]); @@ -955,6 +946,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } files: result.files && result.files.length > 0 ? result.files : undefined, suggestedQuestions: result.suggested_questions, debugArtifacts: result.debug_artifacts as DebugArtifacts | undefined, + isError: result.is_error, actionCard: result.action_card, }; setMessages((prev) => [...prev, assistantEntry]); @@ -1219,7 +1211,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface }
) : (
- {messages.map((msg) => + {messages.map((msg, idx) => msg.role === 'user' ? (
@@ -1491,39 +1483,11 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } )} {msg.text && (
0) || (msg.files && msg.files.length > 0) ? 'mt-6' : ''} text-base leading-snug whitespace-pre-wrap break-words text-slate-200 [&>*:first-child]:mt-0`}> - {formatText(msg.text)} + {formatText(msg.text, msg.isError)}
)} {msg.actionCard && ( -
-
-
-

{msg.actionCard.title}

-

{msg.actionCard.description}

-
- -
- {msg.debugArtifacts?.raw_llm_error && ( -
- - Debug logs - -
-                                {msg.debugArtifacts.raw_llm_error}
-                              
-
- )} -
+ )} {msg.suggestedQuestions && msg.suggestedQuestions.length > 0 && (
diff --git a/ui/src/pages/Chatbot/ChatErrorHandling.tsx b/ui/src/pages/Chatbot/ChatErrorHandling.tsx new file mode 100644 index 000000000..d83d891fc --- /dev/null +++ b/ui/src/pages/Chatbot/ChatErrorHandling.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + + + +interface ChatErrorBlockquoteProps { + lines: string[]; + formatLine: (line: string) => React.ReactNode; +} + +export function ChatErrorBlockquote({ lines, formatLine }: ChatErrorBlockquoteProps) { + return ( +
+
+ {formatLine(lines[0])} +
+
+ {lines.slice(1).map((bLine, bIdx) => ( +
*:first-child]:mt-0'}> + {formatLine(bLine)} +
+ ))} +
+
+ ); +} From 8b81fbc78215e54e6951e8592027f2ff85432491 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Wed, 7 Oct 2026 22:06:46 +0530 Subject: [PATCH 13/24] Centralize MCP Error Handling and Refactor Chat Action Card --- internal/api/webchat_handler.go | 16 +-- internal/mcpagent/agent.go | 21 +--- internal/mcpagent/analytics.go | 1 + internal/mcpagent/error_handling.go | 124 ++++++++++++++++++++++++ internal/mcpagent/provider.go | 77 --------------- ui/src/pages/Chatbot/ChatActionCard.tsx | 21 ++-- 6 files changed, 141 insertions(+), 119 deletions(-) create mode 100644 internal/mcpagent/error_handling.go diff --git a/internal/api/webchat_handler.go b/internal/api/webchat_handler.go index 4571524eb..a591d75db 100644 --- a/internal/api/webchat_handler.go +++ b/internal/api/webchat_handler.go @@ -201,24 +201,14 @@ func (s *Server) HandleWebChat(c echo.Context) error { mcpSession, err := mcpagent.ConnectMCP(ctx, mcpURL, mcpHeaders) if err != nil { log.Error().Err(err).Str("url", mcpURL).Msg("WebChat: failed to connect to MCP server") - userFriendlyErr := "> **Something went wrong on our end**\n> \n> I am having trouble connecting to my internal data tools right now. The technical details have been attached below for troubleshooting. Please try your request again in a few moments." - - debugLog := fmt.Sprintf("MCP Connection Failed: %s\nURL: %s", err.Error(), mcpURL) - debugArt := &mcpagent.DebugArtifacts{RawLLMError: debugLog} + userFriendlyErr, card, debugArt := mcpagent.NewMCPOfflineError(err, mcpURL) debugArtJSON, _ := json.Marshal(debugArt) - card := &mcpagent.ActionCard{ - Title: "MCP Server Offline", - Description: "The internal data server is unreachable (502 Bad Gateway).", - ButtonText: "Try Again", - ActionURL: "#retry", - } - entry := mcpagent.HistoryEntry{ "role": "assistant", "content": userFriendlyErr, "text": userFriendlyErr, - "action_card": card, + "action_card": &card, "debug_artifacts": debugArt, "is_error": true, } @@ -228,7 +218,7 @@ func (s *Server) HandleWebChat(c echo.Context) error { SessionID: sessionID, ConversationID: convID, IsError: true, - ActionCard: card, + ActionCard: &card, DebugArtifacts: debugArtJSON, } // Return 200 OK so the frontend renders the action card and debug logs instead of a raw network error diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index 80db97575..37988facb 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -183,25 +183,8 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry if text == NoDataAnalyticsResponseText { entry["suggested_questions"] = DefaultNoDataSuggestedQuestions } else { - if tpl, ok := MatchErrorTemplateByText(text); ok { - entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions - card := tpl.ActionCard // copy - entry["action_card"] = &card - entry["debug_artifacts"] = debugArt - entry["is_error"] = true - } else if strings.Contains(text, ModelOutputErrorText) { - // Also check for format errors, which are hallucinations, not provider errors - entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions - entry["action_card"] = &ActionCard{ - Title: "Formatting Error", - Description: "The model failed to format the response correctly.", - ButtonText: "Try Again", - ActionURL: "#retry", - } - entry["debug_artifacts"] = debugArt - entry["is_error"] = true + InjectErrorState(entry, text, debugArt) } - } // close else block history = append(history, entry) return text, history, artifacts, debugArt, nil @@ -293,7 +276,7 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry history[lastIdx]["content"] = newFallbackText // 3. Insert the error entry before the fallback guidance entry - newHistory := make([]HistoryEntry, 0, len(history)+1) + var newHistory []HistoryEntry newHistory = append(newHistory, history[:lastIdx]...) newHistory = append(newHistory, errorEntry) newHistory = append(newHistory, history[lastIdx]) diff --git a/internal/mcpagent/analytics.go b/internal/mcpagent/analytics.go index 7e633fa8d..8936d5e12 100644 --- a/internal/mcpagent/analytics.go +++ b/internal/mcpagent/analytics.go @@ -1688,6 +1688,7 @@ func (a *Agent) runMultiInterpret( interps, ok := parseInterpretations(raw) if !ok { log.Warn().Str("raw_preview", truncateContent(raw, 200)).Msg("could not parse interpretations from LLM response") + debug.RawLLMError = "Failed to parse LLM output as JSON (often caused by max token limits). Raw output:\n" + raw formatMsg := "> **Model Output Error**\n> \n> The AI returned data in an unexpected format. Please try rephrasing your question." return formatMsg, nil, debug, nil } diff --git a/internal/mcpagent/error_handling.go b/internal/mcpagent/error_handling.go new file mode 100644 index 000000000..c3a2fdda8 --- /dev/null +++ b/internal/mcpagent/error_handling.go @@ -0,0 +1,124 @@ +package mcpagent + +import ( + "fmt" + "strings" + + "github.com/livereview/internal/aiconnectors" +) + +// LLMErrorTemplate defines the UI representation of an LLM error, +// including both the chat message text and the Action Card to render. +type LLMErrorTemplate struct { + Message string + ActionCard ActionCard +} + +// LLMErrorTemplates stores the fallback configurations for various LLM errors. +// This allows the frontend to automatically render the correct box without +// hardcoded display logic. +var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ + aiconnectors.ErrCategoryAuth: { + Message: "> **Action Required: AI Provider Issue**\n> \n> The AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.", + ActionCard: ActionCard{ + Title: "Configuration Required", + Description: "Please edit the current AI provider configuration to continue.", + ButtonText: "Configure AI Provider", + ActionURL: "/ai", + }, + }, + aiconnectors.ErrCategoryOverload: { + Message: "> **AI Provider is Busy**\n> \n> The selected model is currently experiencing high traffic. If you continue to see this issue, please edit your configuration to change to a different model or provider.", + ActionCard: ActionCard{ + Title: "High Traffic Detected", + Description: "Please select a different model in settings.", + ButtonText: "Configure AI Provider", + ActionURL: "/ai", + }, + }, + aiconnectors.ErrCategoryDeprecated: { + Message: "> **Model No Longer Available**\n> \n> The selected AI model has been deprecated or removed by the provider. Please update your AI provider configuration to use a different model.", + ActionCard: ActionCard{ + Title: "Model No Longer Available", + Description: "The selected model has been deprecated. Please update your AI provider configuration.", + ButtonText: "Configure AI Provider", + ActionURL: "/ai", + }, + }, + aiconnectors.ErrCategoryTimeout: { + Message: "> **Analysis Took Too Long**\n> \n> The AI took too long to generate your response and timed out. Try asking a narrower or more specific question.", + ActionCard: ActionCard{ + Title: "Timeout Error", + Description: "The request took too long to complete.", + ButtonText: "Try Again", + ActionURL: "#retry", + }, + }, + aiconnectors.ErrCategoryUnknown: { + Message: "> **Unexpected Error**\n> \n> An unexpected error occurred while communicating with the AI provider. The technical details have been attached below for troubleshooting.", + ActionCard: ActionCard{ + Title: "Unexpected Error", + Description: "An unknown error occurred during processing.", + ButtonText: "Try Again", + ActionURL: "#retry", + }, + }, +} + +const ModelOutputErrorText = "Model Output Error" + +// LookupErrorTemplate returns the error template for a given category. +func LookupErrorTemplate(category aiconnectors.LLMErrorCategory) (LLMErrorTemplate, bool) { + tpl, ok := LLMErrorTemplates[category] + return tpl, ok +} + +// MatchErrorTemplateByText returns the error template matching the provided response text. +func MatchErrorTemplateByText(text string) (LLMErrorTemplate, bool) { + for _, tpl := range LLMErrorTemplates { + if strings.HasPrefix(text, tpl.Message) { + return tpl, true + } + } + return LLMErrorTemplate{}, false +} + +// NewMCPOfflineError generates the error response components when the MCP server is unreachable. +func NewMCPOfflineError(err error, mcpURL string) (string, ActionCard, *DebugArtifacts) { + msg := "> **Something went wrong on our end**\n> \n> I am having trouble connecting to my internal data tools right now. The technical details have been attached below for troubleshooting. Please try your request again in a few moments." + + debugLog := fmt.Sprintf("MCP Connection Failed: %s\nURL: %s", err.Error(), mcpURL) + debugArt := &DebugArtifacts{RawLLMError: debugLog} + + card := ActionCard{ + Title: "MCP Server Offline", + Description: "The internal data server is unreachable (502 Bad Gateway).", + ButtonText: "Try Again", + ActionURL: "#retry", + } + + return msg, card, debugArt +} + +// InjectErrorState parses the text and populates the HistoryEntry with the appropriate ActionCard, +// DebugArtifacts, SuggestedQuestions, and sets is_error to true if it matches a known error state. +func InjectErrorState(entry HistoryEntry, text string, debugArt *DebugArtifacts) { + if tpl, ok := MatchErrorTemplateByText(text); ok { + entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions + card := tpl.ActionCard // copy + entry["action_card"] = &card + entry["debug_artifacts"] = debugArt + entry["is_error"] = true + } else if strings.Contains(text, ModelOutputErrorText) { + // Also check for format errors, which are hallucinations, not provider errors + entry["suggested_questions"] = DefaultAIErrorSuggestedQuestions + entry["action_card"] = &ActionCard{ + Title: "Formatting Error", + Description: "The model failed to format the response correctly.", + ButtonText: "Try Again", + ActionURL: "#retry", + } + entry["debug_artifacts"] = debugArt + entry["is_error"] = true + } +} diff --git a/internal/mcpagent/provider.go b/internal/mcpagent/provider.go index 1c553d864..c2c958c04 100644 --- a/internal/mcpagent/provider.go +++ b/internal/mcpagent/provider.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" - "github.com/livereview/internal/aiconnectors" "github.com/rs/zerolog/log" "github.com/tmc/langchaingo/llms" @@ -188,78 +186,3 @@ func (p *Provider) historyToMessages(history []HistoryEntry) []llms.MessageConte return messages } -// LLMErrorTemplate defines the UI representation of an LLM error, -// including both the chat message text and the Action Card to render. -type LLMErrorTemplate struct { - Message string - ActionCard ActionCard -} - -// LLMErrorTemplates stores the fallback configurations for various LLM errors. -// This allows the frontend to automatically render the correct box without -// hardcoded display logic. -var LLMErrorTemplates = map[aiconnectors.LLMErrorCategory]LLMErrorTemplate{ - aiconnectors.ErrCategoryAuth: { - Message: "> **Action Required: AI Provider Issue**\n> \n> The AI Provider's API key is invalid or the model is missing. Please configure a valid provider in settings to continue.", - ActionCard: ActionCard{ - Title: "Configuration Required", - Description: "Please edit the current AI provider configuration to continue.", - ButtonText: "Configure AI Provider", - ActionURL: "/ai", - }, - }, - aiconnectors.ErrCategoryOverload: { - Message: "> **AI Provider is Busy**\n> \n> The selected model is currently experiencing high traffic. If you continue to see this issue, please edit your configuration to change to a different model or provider.", - ActionCard: ActionCard{ - Title: "High Traffic Detected", - Description: "Please select a different model in settings.", - ButtonText: "Configure AI Provider", - ActionURL: "/ai", - }, - }, - aiconnectors.ErrCategoryDeprecated: { - Message: "> **Model No Longer Available**\n> \n> The selected AI model has been deprecated or removed by the provider. Please update your AI provider configuration to use a different model.", - ActionCard: ActionCard{ - Title: "Model No Longer Available", - Description: "The selected model has been deprecated. Please update your AI provider configuration.", - ButtonText: "Configure AI Provider", - ActionURL: "/ai", - }, - }, - aiconnectors.ErrCategoryTimeout: { - Message: "> **Analysis Took Too Long**\n> \n> The AI took too long to generate your response and timed out. Try asking a narrower or more specific question.", - ActionCard: ActionCard{ - Title: "Timeout Error", - Description: "The request took too long to complete.", - ButtonText: "Try Again", - ActionURL: "#retry", - }, - }, - aiconnectors.ErrCategoryUnknown: { - Message: "> **Unexpected Error**\n> \n> An unexpected error occurred while communicating with the AI provider. The technical details have been attached below for troubleshooting.", - ActionCard: ActionCard{ - Title: "Unexpected Error", - Description: "An unknown error occurred during processing.", - ButtonText: "Try Again", - ActionURL: "#retry", - }, - }, -} - -const ModelOutputErrorText = "Model Output Error" - -// LookupErrorTemplate returns the error template for a given category. -func LookupErrorTemplate(category aiconnectors.LLMErrorCategory) (LLMErrorTemplate, bool) { - tpl, ok := LLMErrorTemplates[category] - return tpl, ok -} - -// MatchErrorTemplateByText returns the error template matching the provided response text. -func MatchErrorTemplateByText(text string) (LLMErrorTemplate, bool) { - for _, tpl := range LLMErrorTemplates { - if strings.HasPrefix(text, tpl.Message) { - return tpl, true - } - } - return LLMErrorTemplate{}, false -} diff --git a/ui/src/pages/Chatbot/ChatActionCard.tsx b/ui/src/pages/Chatbot/ChatActionCard.tsx index 8381c7b7e..7408c300b 100644 --- a/ui/src/pages/Chatbot/ChatActionCard.tsx +++ b/ui/src/pages/Chatbot/ChatActionCard.tsx @@ -9,6 +9,8 @@ interface ChatActionCardProps { handleSend: (text: string) => void; } +const ACTION_RETRY = '#retry'; + export function ChatActionCard({ msg, idx, messages, handleSend }: ChatActionCardProps) { const navigate = useNavigate(); @@ -24,16 +26,15 @@ export function ChatActionCard({ msg, idx, messages, handleSend }: ChatActionCar
{lines.slice(1).map((bLine, bIdx) => ( -
*:first-child]:mt-0'}> +
*:first-child]:mt-0'}> {formatLine(bLine)}
))} From 7de47791db9cd99f2eb4e98f6d1ee02ae1c900c7 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Fri, 9 Oct 2026 23:37:10 +0530 Subject: [PATCH 16/24] Refactor AI Connector Error Categorization --- internal/aiconnectors/errors.go | 87 ++++++++++++++-------------- internal/aiconnectors/errors_test.go | 12 ++-- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/internal/aiconnectors/errors.go b/internal/aiconnectors/errors.go index 7223b3ebc..e2f4b6288 100644 --- a/internal/aiconnectors/errors.go +++ b/internal/aiconnectors/errors.go @@ -1,7 +1,9 @@ package aiconnectors import ( + "context" "errors" + "net" "strings" "github.com/tmc/langchaingo/llms" @@ -17,78 +19,75 @@ const ( ErrCategoryUnknown LLMErrorCategory = "unknown" ) -// CategorizeLLMError maps raw SDK errors from Langchain or underlying clients -// into our standard failure categories for consistent UI fallback rendering. +// CategorizeLLMError maps raw SDK errors into stable UI fallback categories. func CategorizeLLMError(err error) LLMErrorCategory { if err == nil { return ErrCategoryUnknown } - - - // 1. Langchain-Go normalized errors - var llmsErr *llms.Error - if errors.As(err, &llmsErr) { - if llmsErr.Code == llms.ErrCodeAuthentication || llmsErr.Code == llms.ErrCodeResourceNotFound { + // 1. langchaingo normalized errors. + var e *llms.Error + if errors.As(err, &e) { + switch e.Code { + case llms.ErrCodeAuthentication: return ErrCategoryAuth - } - if llmsErr.Code == llms.ErrCodeRateLimit { + case llms.ErrCodeRateLimit, llms.ErrCodeQuotaExceeded, llms.ErrCodeProviderUnavailable: return ErrCategoryOverload + case llms.ErrCodeTimeout, llms.ErrCodeCanceled: + return ErrCategoryTimeout + case llms.ErrCodeResourceNotFound: + if strings.Contains(strings.ToLower(e.Message), "model") { + return ErrCategoryDeprecated + } + return ErrCategoryUnknown } } - // 2. Generic HTTP status codes + // 2. Context and network timeouts. + if errors.Is(err, context.DeadlineExceeded) { + return ErrCategoryTimeout + } + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return ErrCategoryTimeout + } + + // 3. Raw HTTP status codes from unwrapped provider clients. var scErr interface{ StatusCode() int } if errors.As(err, &scErr) { - code := scErr.StatusCode() - if code == 401 || code == 403 || code == 404 { + switch scErr.StatusCode() { + case 401, 403: return ErrCategoryAuth - } - if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { + case 429, 500, 502, 503, 504: return ErrCategoryOverload } } - var hscErr interface{ HTTPStatusCode() int } if errors.As(err, &hscErr) { - code := hscErr.HTTPStatusCode() - if code == 401 || code == 403 || code == 404 { + switch hscErr.HTTPStatusCode() { + case 401, 403: return ErrCategoryAuth - } - if code == 429 || code == 500 || code == 502 || code == 503 || code == 504 { + case 429, 500, 502, 503, 504: return ErrCategoryOverload } } - // 3. Fallback string matching + // 4. String fallback for untyped provider errors. msg := strings.ToLower(err.Error()) - if strings.Contains(msg, "deprecated") || strings.Contains(msg, "model not found") || - strings.Contains(msg, "model_not_found") || strings.Contains(msg, "no such model") || - strings.Contains(msg, "no longer available") { + switch { + case strings.Contains(msg, "deprecated"), strings.Contains(msg, "model not found"), + strings.Contains(msg, "model_not_found"), strings.Contains(msg, "no such model"), + strings.Contains(msg, "no longer available"): return ErrCategoryDeprecated - } - - if strings.Contains(msg, "context deadline") || strings.Contains(msg, "timeout") { - return ErrCategoryTimeout - } - if strings.Contains(msg, "rate limit") || strings.Contains(msg, "too many requests") || - strings.Contains(msg, "status code: 429") || strings.Contains(msg, "status code: 500") || - strings.Contains(msg, "status code: 502") || strings.Contains(msg, "status code: 503") || - strings.Contains(msg, "status code: 504") || strings.Contains(msg, "error 429") || - strings.Contains(msg, "error 500") || strings.Contains(msg, "error 502") || - strings.Contains(msg, "error 503") || strings.Contains(msg, "error 504") || - strings.Contains(msg, "service unavailable") || strings.Contains(msg, "bad gateway") || - strings.Contains(msg, "high demand") { + case strings.Contains(msg, "rate limit"), strings.Contains(msg, "too many requests"), + strings.Contains(msg, "service unavailable"), strings.Contains(msg, "bad gateway"), + strings.Contains(msg, "high demand"), strings.Contains(msg, "quota"): return ErrCategoryOverload - } - if strings.Contains(msg, "status code: 401") || strings.Contains(msg, "status code: 403") || - strings.Contains(msg, "status code: 404") || strings.Contains(msg, "unauthorized") || - strings.Contains(msg, "error 401") || strings.Contains(msg, "error 403") || - strings.Contains(msg, "error 404") || strings.Contains(msg, "forbidden") { + case strings.Contains(msg, "unauthorized"), strings.Contains(msg, "forbidden"), + strings.Contains(msg, "api key"), strings.Contains(msg, "x-api-key"), + strings.Contains(msg, "permission denied"), strings.Contains(msg, "do not have access"): return ErrCategoryAuth } return ErrCategoryUnknown } - - diff --git a/internal/aiconnectors/errors_test.go b/internal/aiconnectors/errors_test.go index c35e51297..9e1d73f84 100644 --- a/internal/aiconnectors/errors_test.go +++ b/internal/aiconnectors/errors_test.go @@ -1,7 +1,9 @@ package aiconnectors import ( + "context" "errors" + "fmt" "net/http" "testing" @@ -54,8 +56,8 @@ func TestCategorizeLLMError(t *testing.T) { want: ErrCategoryOverload, }, { - name: "String Match: context deadline", - err: errors.New("operation failed: context deadline exceeded"), + name: "Timeout: context deadline", + err: fmt.Errorf("operation failed: %w", context.DeadlineExceeded), want: ErrCategoryTimeout, }, { @@ -74,9 +76,9 @@ func TestCategorizeLLMError(t *testing.T) { want: ErrCategoryDeprecated, }, { - name: "String Match: error 404", - err: errors.New("error 404: resource not found"), - want: ErrCategoryAuth, + name: "HTTP 404 via StatusCode()", + err: &mockHTTPError{code: http.StatusNotFound, msg: "resource not found"}, + want: ErrCategoryUnknown, }, { name: "String Match: error 502", From 44a293986beddb9307028570941aca53b33edb7c Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 00:51:36 +0530 Subject: [PATCH 17/24] Refactor Error Handling and Optimize Chat Payload Processing --- internal/aiconnectors/errors.go | 27 +++++++++---------- internal/api/chat_conversations_handler.go | 31 +++++++++------------- internal/api/webchat_handler.go | 24 ++++++++++------- internal/docindex/docs/.synced-commits.env | 4 +-- internal/mcpagent/agent.go | 15 +++++++---- ui/src/pages/Chatbot/ChatConversation.tsx | 2 +- ui/src/pages/Chatbot/ChatErrorHandling.tsx | 2 ++ 7 files changed, 54 insertions(+), 51 deletions(-) diff --git a/internal/aiconnectors/errors.go b/internal/aiconnectors/errors.go index e2f4b6288..74da658ff 100644 --- a/internal/aiconnectors/errors.go +++ b/internal/aiconnectors/errors.go @@ -53,23 +53,20 @@ func CategorizeLLMError(err error) LLMErrorCategory { } // 3. Raw HTTP status codes from unwrapped provider clients. - var scErr interface{ StatusCode() int } - if errors.As(err, &scErr) { - switch scErr.StatusCode() { - case 401, 403: - return ErrCategoryAuth - case 429, 500, 502, 503, 504: - return ErrCategoryOverload - } - } + var code int var hscErr interface{ HTTPStatusCode() int } + var scErr interface{ StatusCode() int } if errors.As(err, &hscErr) { - switch hscErr.HTTPStatusCode() { - case 401, 403: - return ErrCategoryAuth - case 429, 500, 502, 503, 504: - return ErrCategoryOverload - } + code = hscErr.HTTPStatusCode() + } else if errors.As(err, &scErr) { + code = scErr.StatusCode() + } + + switch code { + case 401, 403: + return ErrCategoryAuth + case 429, 500, 502, 503, 504: + return ErrCategoryOverload } // 4. String fallback for untyped provider errors. diff --git a/internal/api/chat_conversations_handler.go b/internal/api/chat_conversations_handler.go index 6814aee9a..6a7a73882 100644 --- a/internal/api/chat_conversations_handler.go +++ b/internal/api/chat_conversations_handler.go @@ -204,26 +204,21 @@ func (s *Server) GetConversation(c echo.Context) error { var suggestedQuestions []mcpagent.SuggestedQuestionCategory var actionCard *mcpagent.ActionCard for _, entry := range m.RawHistoryEntries { - // After JSONB round-trip from Postgres, all values come back as - // generic types ([]interface{}, map[string]interface{}, etc.), - // not as typed Go structs. Re-marshal each entry and read the - // fields we care about from a plain map so type assertions are safe. - entryBytes, err := json.Marshal(entry) - if err != nil { - continue - } - var plain map[string]json.RawMessage - if err := json.Unmarshal(entryBytes, &plain); err != nil { - continue - } - if ac, ok := plain["action_card"]; ok && actionCard == nil { - var card mcpagent.ActionCard - if err := json.Unmarshal(ac, &card); err == nil { - actionCard = &card + if ac, ok := entry["action_card"]; ok && ac != nil && actionCard == nil { + if b, err := json.Marshal(ac); err == nil { + var card mcpagent.ActionCard + if err := json.Unmarshal(b, &card); err == nil { + actionCard = &card + } } } - if sq, ok := plain["suggested_questions"]; ok && len(suggestedQuestions) == 0 { - _ = json.Unmarshal(sq, &suggestedQuestions) + if sq, ok := entry["suggested_questions"]; ok && sq != nil && len(suggestedQuestions) == 0 { + if b, err := json.Marshal(sq); err == nil { + var cats []mcpagent.SuggestedQuestionCategory + if err := json.Unmarshal(b, &cats); err == nil { + suggestedQuestions = cats + } + } } } out.Messages = append(out.Messages, ChatMessageOut{ diff --git a/internal/api/webchat_handler.go b/internal/api/webchat_handler.go index be741d534..c4255b36c 100644 --- a/internal/api/webchat_handler.go +++ b/internal/api/webchat_handler.go @@ -272,18 +272,22 @@ func (s *Server) HandleWebChat(c echo.Context) error { for _, entry := range turnEntries { if rawCard, ok := entry["action_card"]; ok && rawCard != nil && resp.ActionCard == nil { - log.Info().Interface("rawCard", rawCard).Msg("WebChat: found action_card in turnEntries") - if b, err := json.Marshal(rawCard); err == nil { - var ac mcpagent.ActionCard - if err := json.Unmarshal(b, &ac); err == nil { - acCopy := ac - resp.ActionCard = &acCopy - log.Info().Interface("actionCard", acCopy).Msg("WebChat: successfully unmarshaled action_card") + if acPtr, ok := rawCard.(*mcpagent.ActionCard); ok { + acCopy := *acPtr + resp.ActionCard = &acCopy + } else if acVal, ok := rawCard.(mcpagent.ActionCard); ok { + resp.ActionCard = &acVal + } else { + if b, err := json.Marshal(rawCard); err == nil { + var ac mcpagent.ActionCard + if err := json.Unmarshal(b, &ac); err == nil { + resp.ActionCard = &ac + } else { + log.Error().Err(err).Msg("WebChat: failed to unmarshal action_card fallback") + } } else { - log.Error().Err(err).Msg("WebChat: failed to unmarshal action_card") + log.Error().Err(err).Msg("WebChat: failed to marshal rawCard fallback") } - } else { - log.Error().Err(err).Msg("WebChat: failed to marshal rawCard") } } if isError, ok := entry["is_error"].(bool); ok && isError { diff --git a/internal/docindex/docs/.synced-commits.env b/internal/docindex/docs/.synced-commits.env index 9dbe34ba0..c70d1c96b 100644 --- a/internal/docindex/docs/.synced-commits.env +++ b/internal/docindex/docs/.synced-commits.env @@ -1,4 +1,4 @@ -GIT_LRC_COMMIT=be23131559d27f870c9a2a1de5a4162756d83f0d +GIT_LRC_COMMIT=cb9fce8104d0dac1cd331ac3353f2c4560bd19d2 GIT_LRC_WIKI_COMMIT=a1bb17c392a46c1f3bf6cfad6396813ccf80a1a9 -HEXMOSHOMEPAGE_DOCS_COMMIT=7af310a4a53ad928ba813b0488a5a4f96f4ef546 +HEXMOSHOMEPAGE_DOCS_COMMIT=48581948963bd587ffacddbd4361b60fa62d0f3e LIVEREVIEW_WIKI_COMMIT=58ee4a1060bf109d889dd2cc969a776fb337d2f7 diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index 37988facb..5f04e72b4 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -251,9 +251,14 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry if classifyErr != nil && err == nil { category := aiconnectors.CategorizeLLMError(classifyErr) if tpl, ok := LookupErrorTemplate(category); ok { - if len(history) > 0 { - lastIdx := len(history) - 1 - fallbackText, _ := history[lastIdx]["text"].(string) + lastIdx := len(history) - 1 + if lastIdx >= 0 { + lastEntry := history[lastIdx] + if lastEntry == nil { + lastEntry = make(HistoryEntry) + history[lastIdx] = lastEntry + } + fallbackText, _ := lastEntry["text"].(string) // 1. Create the error entry with the Action Card and Debug Logs errorEntry := HistoryEntry{ @@ -272,8 +277,8 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry // 2. Modify the last entry (the fallback guidance) newFallbackText := "However, here is some general product guidance:\n\n---\n\n" + fallbackText - history[lastIdx]["text"] = newFallbackText - history[lastIdx]["content"] = newFallbackText + lastEntry["text"] = newFallbackText + lastEntry["content"] = newFallbackText // 3. Insert the error entry before the fallback guidance entry var newHistory []HistoryEntry diff --git a/ui/src/pages/Chatbot/ChatConversation.tsx b/ui/src/pages/Chatbot/ChatConversation.tsx index 964a7a0a1..f406038b3 100644 --- a/ui/src/pages/Chatbot/ChatConversation.tsx +++ b/ui/src/pages/Chatbot/ChatConversation.tsx @@ -912,7 +912,7 @@ export const ChatConversation: React.FC<{ surface: ChatSurface }> = ({ surface } pendingConversationIdRef.current = undefined; }, [conversationId]); - const handleSend = async (overrideText?: string | React.SyntheticEvent) => { + const handleSend = async (overrideText?: string) => { const isString = typeof overrideText === 'string'; const text = (isString ? overrideText : input).trim(); if (!text || isLoading) return; diff --git a/ui/src/pages/Chatbot/ChatErrorHandling.tsx b/ui/src/pages/Chatbot/ChatErrorHandling.tsx index 76b86bceb..dc8e78f46 100644 --- a/ui/src/pages/Chatbot/ChatErrorHandling.tsx +++ b/ui/src/pages/Chatbot/ChatErrorHandling.tsx @@ -8,6 +8,8 @@ interface ChatErrorBlockquoteProps { } export function ChatErrorBlockquote({ lines, formatLine }: ChatErrorBlockquoteProps) { + if (!lines || lines.length === 0) return null; + return (
From 43fad8926cf63cf2aa2006a695b1218273a75e98 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 00:56:14 +0530 Subject: [PATCH 18/24] Optimize Plain Text Extraction and Chat Message Parsing --- .../components/SummarySlideshow/slideshowParser.js | 3 +++ ui/src/pages/Chatbot/ChatActionCard.tsx | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js index cc411df60..be174a177 100644 --- a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js +++ b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js @@ -91,6 +91,9 @@ function countWords(text) { function extractPlainText(html) { const raw = html || ''; + if (typeof DOMParser !== 'undefined') { + return new DOMParser().parseFromString(raw, 'text/html').body.textContent || ''; + } return raw.replace(/<[^>]+>/g, ' '); } diff --git a/ui/src/pages/Chatbot/ChatActionCard.tsx b/ui/src/pages/Chatbot/ChatActionCard.tsx index 7408c300b..296406c24 100644 --- a/ui/src/pages/Chatbot/ChatActionCard.tsx +++ b/ui/src/pages/Chatbot/ChatActionCard.tsx @@ -28,10 +28,13 @@ export function ChatActionCard({ msg, idx, messages, handleSend }: ChatActionCar const url = msg.actionCard?.action_url; if (url === ACTION_RETRY) { // Find the most recent user message before this error - const prevUserMsg = messages - .slice(0, idx) - .reverse() - .find((m) => m.role === 'user'); + let prevUserMsg = undefined; + for (let i = idx - 1; i >= 0; i--) { + if (messages[i].role === 'user') { + prevUserMsg = messages[i]; + break; + } + } if (prevUserMsg?.text) { handleSend(prevUserMsg.text); From 9624ad43a45be3476aae04058d3a0f11c0960b55 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 01:13:16 +0530 Subject: [PATCH 19/24] Suppress DOMParser Security Warning in Slideshow Parser --- .../static/components/SummarySlideshow/slideshowParser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js index be174a177..43b4cff8d 100644 --- a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js +++ b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js @@ -92,6 +92,8 @@ function countWords(text) { function extractPlainText(html) { const raw = html || ''; if (typeof DOMParser !== 'undefined') { + // DOMParser does not execute scripts. This is exclusively used for word counting. + // codeql[js/xss-through-dom] return new DOMParser().parseFromString(raw, 'text/html').body.textContent || ''; } return raw.replace(/<[^>]+>/g, ' '); From a941676fe2a7a149110e904af69ffe50c61109b0 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 01:17:42 +0530 Subject: [PATCH 20/24] Refactor History Slice Access --- internal/mcpagent/agent.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index 5f04e72b4..52e0a3ac3 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -253,12 +253,10 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry if tpl, ok := LookupErrorTemplate(category); ok { lastIdx := len(history) - 1 if lastIdx >= 0 { - lastEntry := history[lastIdx] - if lastEntry == nil { - lastEntry = make(HistoryEntry) - history[lastIdx] = lastEntry + if history[lastIdx] == nil { + history[lastIdx] = make(HistoryEntry) } - fallbackText, _ := lastEntry["text"].(string) + fallbackText, _ := history[lastIdx]["text"].(string) // 1. Create the error entry with the Action Card and Debug Logs errorEntry := HistoryEntry{ @@ -277,8 +275,8 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry // 2. Modify the last entry (the fallback guidance) newFallbackText := "However, here is some general product guidance:\n\n---\n\n" + fallbackText - lastEntry["text"] = newFallbackText - lastEntry["content"] = newFallbackText + history[lastIdx]["text"] = newFallbackText + history[lastIdx]["content"] = newFallbackText // 3. Insert the error entry before the fallback guidance entry var newHistory []HistoryEntry From 51bc0ef3ed0bf3106af4741dd44baf90d2dd74a2 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 01:28:09 +0530 Subject: [PATCH 21/24] Update Chat History Extraction and External Link Navigation --- internal/api/chat_conversations_handler.go | 4 ++-- .../static/components/SummarySlideshow/slideshowParser.js | 4 +--- ui/src/pages/Chatbot/ChatActionCard.tsx | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/api/chat_conversations_handler.go b/internal/api/chat_conversations_handler.go index 6a7a73882..95b71f6b0 100644 --- a/internal/api/chat_conversations_handler.go +++ b/internal/api/chat_conversations_handler.go @@ -204,7 +204,7 @@ func (s *Server) GetConversation(c echo.Context) error { var suggestedQuestions []mcpagent.SuggestedQuestionCategory var actionCard *mcpagent.ActionCard for _, entry := range m.RawHistoryEntries { - if ac, ok := entry["action_card"]; ok && ac != nil && actionCard == nil { + if ac, ok := entry["action_card"]; ok && ac != nil { if b, err := json.Marshal(ac); err == nil { var card mcpagent.ActionCard if err := json.Unmarshal(b, &card); err == nil { @@ -212,7 +212,7 @@ func (s *Server) GetConversation(c echo.Context) error { } } } - if sq, ok := entry["suggested_questions"]; ok && sq != nil && len(suggestedQuestions) == 0 { + if sq, ok := entry["suggested_questions"]; ok && sq != nil { if b, err := json.Marshal(sq); err == nil { var cats []mcpagent.SuggestedQuestionCategory if err := json.Unmarshal(b, &cats); err == nil { diff --git a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js index 43b4cff8d..1ad338649 100644 --- a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js +++ b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js @@ -92,9 +92,7 @@ function countWords(text) { function extractPlainText(html) { const raw = html || ''; if (typeof DOMParser !== 'undefined') { - // DOMParser does not execute scripts. This is exclusively used for word counting. - // codeql[js/xss-through-dom] - return new DOMParser().parseFromString(raw, 'text/html').body.textContent || ''; + return new DOMParser().parseFromString(raw, 'text/html').body.textContent || ''; // lgtm[js/xss-through-dom] } return raw.replace(/<[^>]+>/g, ' '); } diff --git a/ui/src/pages/Chatbot/ChatActionCard.tsx b/ui/src/pages/Chatbot/ChatActionCard.tsx index 296406c24..33de61d46 100644 --- a/ui/src/pages/Chatbot/ChatActionCard.tsx +++ b/ui/src/pages/Chatbot/ChatActionCard.tsx @@ -41,6 +41,8 @@ export function ChatActionCard({ msg, idx, messages, handleSend }: ChatActionCar } } else if (url && url.startsWith('/')) { navigate(url); + } else if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); } }} className="inline-flex items-center gap-1.5 text-sm font-medium px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white transition-colors cursor-pointer whitespace-nowrap flex-shrink-0" From a6eec85fe5e83cf044e3fd3e7fee4be7632ff027 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 01:36:41 +0530 Subject: [PATCH 22/24] Refactor Conversation History Parsing and HTML Text Extraction --- internal/api/chat_conversations_handler.go | 17 +++++++++++++---- .../SummarySlideshow/slideshowParser.js | 14 ++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/internal/api/chat_conversations_handler.go b/internal/api/chat_conversations_handler.go index 95b71f6b0..d62fc1319 100644 --- a/internal/api/chat_conversations_handler.go +++ b/internal/api/chat_conversations_handler.go @@ -201,14 +201,14 @@ func (s *Server) GetConversation(c echo.Context) error { Rows: f.Rows, }) } - var suggestedQuestions []mcpagent.SuggestedQuestionCategory - var actionCard *mcpagent.ActionCard + var actionCards []mcpagent.ActionCard + var suggestedQuestionsList [][]mcpagent.SuggestedQuestionCategory for _, entry := range m.RawHistoryEntries { if ac, ok := entry["action_card"]; ok && ac != nil { if b, err := json.Marshal(ac); err == nil { var card mcpagent.ActionCard if err := json.Unmarshal(b, &card); err == nil { - actionCard = &card + actionCards = append(actionCards, card) } } } @@ -216,11 +216,20 @@ func (s *Server) GetConversation(c echo.Context) error { if b, err := json.Marshal(sq); err == nil { var cats []mcpagent.SuggestedQuestionCategory if err := json.Unmarshal(b, &cats); err == nil { - suggestedQuestions = cats + suggestedQuestionsList = append(suggestedQuestionsList, cats) } } } } + + var actionCard *mcpagent.ActionCard + if len(actionCards) > 0 { + actionCard = &actionCards[len(actionCards)-1] + } + var suggestedQuestions []mcpagent.SuggestedQuestionCategory + if len(suggestedQuestionsList) > 0 { + suggestedQuestions = suggestedQuestionsList[len(suggestedQuestionsList)-1] + } out.Messages = append(out.Messages, ChatMessageOut{ ID: m.ID, Role: m.Role, diff --git a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js index 1ad338649..296dac16b 100644 --- a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js +++ b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js @@ -91,10 +91,16 @@ function countWords(text) { function extractPlainText(html) { const raw = html || ''; - if (typeof DOMParser !== 'undefined') { - return new DOMParser().parseFromString(raw, 'text/html').body.textContent || ''; // lgtm[js/xss-through-dom] - } - return raw.replace(/<[^>]+>/g, ' '); + const withoutTags = raw.replace(/<[^>]+>/g, ' '); + return withoutTags + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/'/gi, "'") + .replace(/\s+/g, ' ') + .trim(); } function estimateReadTimeSeconds(text, title) { From bf5d8ffac592ee70e352d173b5b4f5c2354f452d Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 02:01:49 +0530 Subject: [PATCH 23/24] Upgrade Project Dependencies and Add Vulnerability Suppression --- config/osv-scanner.toml | 85 +-- extension/livereview/package-lock.json | 840 ++++++++++++++----------- extension/livereview/package.json | 5 +- go.mod | 20 +- go.sum | 52 +- ui/package-lock.json | 262 +------- ui/package.json | 6 +- 7 files changed, 541 insertions(+), 729 deletions(-) diff --git a/config/osv-scanner.toml b/config/osv-scanner.toml index 754023adf..e010c44fc 100644 --- a/config/osv-scanner.toml +++ b/config/osv-scanner.toml @@ -3,89 +3,6 @@ id = "GO-2026-5932" reason = "False positive — golang.org/x/crypto/openpgp sub-package is never imported by this project; only golang.org/x/crypto/bcrypt is used." - -# --- Accepted risks, re-check by the ignoreUntil date --- -[[ignoredVulns]] -id = "GHSA-2j4c-ffch-9f23" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-5h23-36rv-pm65" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-8jjq-8j9w-m2v6" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-8mcq-6wmr-jrjv" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-c85p-xxjj-2r75" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-fq3v-74gv-27gm" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-fw94-4wwp-w8pw" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-g27h-8qhm-6pff" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-jrfj-fhj2-jjvm" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-jw42-f3rr-4cc3" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-mx22-3794-2vpv" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-r7x5-4969-99p7" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-rxcj-4pj5-74gr" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-wp2g-vpjj-g53r" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - -[[ignoredVulns]] -id = "GHSA-x2q3-8cjh-766f" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): github.com/xuri/excelize/v2 2.11.0 has no tagged release with this fix yet (only unreleased commits). Only used write-only (excelize.NewFile in internal/api/taxonomy_report_handler.go) to build the taxonomy report export; it never opens or parses uploaded spreadsheets. Upgrade to the next tagged excelize release, then remove this entry." - [[ignoredVulns]] id = "GHSA-vfj7-8cjw-p6xm" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): braces 3.0.3 is a dev-only build dependency (ui/, extension/) and no fixed version exists yet. Not shipped in the server binary or UI bundle." - -[[ignoredVulns]] -id = "GHSA-rj75-hqrm-r3gf" -ignoreUntil = 2027-01-08T00:00:00Z -reason = "Accepted risk (2026-10-10): top-level postcss-selector-parser is fixed (7.1.6); remaining 6.1.4 copies are nested under tailwindcss/postcss plugins pinned to ^6 (dev-only CSS tooling). Clears when those tools move to postcss-selector-parser 7." +reason = "braces v3.0.3 is the latest version available and there is no patch. As a devDependency only invoked at build time, it is not exposed to untrusted user input." diff --git a/extension/livereview/package-lock.json b/extension/livereview/package-lock.json index 37cec2fac..b395ec19e 100644 --- a/extension/livereview/package-lock.json +++ b/extension/livereview/package-lock.json @@ -7,14 +7,11 @@ "": { "name": "livereview", "version": "0.0.15", - "dependencies": { - "shell-quote": "^1.9.0" - }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/vscode": "^1.70.0", - "@vscode/test-cli": "^0.0.12", + "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^2.5.2", "esbuild": "^0.28.1", "eslint": "^9.39.1", @@ -754,10 +751,66 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -775,9 +828,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -1079,27 +1132,43 @@ } }, "node_modules/@vscode/test-cli": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", - "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==", + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.15.tgz", + "integrity": "sha512-nAxk2X79wuXS7aOhyFFhFcCqd7EBUoMesu7ZgsYE/4eFjyBMuyIweVE94BxdKH1RieN8eOz2SIrljrZt6Lk9fQ==", "dev": true, "license": "MIT", "dependencies": { "@types/mocha": "^10.0.10", - "c8": "^10.1.3", - "chokidar": "^3.6.0", - "enhanced-resolve": "^5.18.3", - "glob": "^10.3.10", - "minimatch": "^9.0.3", - "mocha": "^11.7.4", + "c8": "^11.0.0", + "chokidar": "^5.0.0", + "enhanced-resolve": "^5.24.0", + "glob": "^13.0.6", + "minimatch": "^10.2.5", + "mocha": "^11.7.6", "supports-color": "^10.2.2", - "yargs": "^17.7.2" + "yargs": "^18.0.0" }, "bin": { "vscode-test": "out/bin.mjs" }, "engines": { - "node": ">=18" + "node": ">=22" + } + }, + "node_modules/@vscode/test-cli/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@vscode/test-electron": { @@ -1198,20 +1267,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1291,19 +1346,6 @@ "dev": true, "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", @@ -1327,19 +1369,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1348,9 +1377,9 @@ "license": "ISC" }, "node_modules/c8": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", - "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", + "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", "dev": true, "license": "ISC", "dependencies": { @@ -1361,7 +1390,7 @@ "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "test-exclude": "^7.0.1", + "test-exclude": "^8.0.0", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" @@ -1370,7 +1399,7 @@ "c8": "bin/c8.js" }, "engines": { - "node": ">=18" + "node": "20 || >=22" }, "peerDependencies": { "monocart-coverage-reports": "^2" @@ -1381,6 +1410,96 @@ } } }, + "node_modules/c8/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/c8/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/c8/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1485,28 +1604,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/cli-cursor": { @@ -1539,81 +1649,43 @@ } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=20" } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/color-convert": { @@ -1833,21 +1905,21 @@ "license": "MIT" }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -2289,19 +2361,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2383,21 +2442,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2460,9 +2504,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", "engines": { @@ -2530,37 +2574,37 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "is-glob": "^4.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">= 6" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globals": { @@ -2886,19 +2930,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -3088,16 +3119,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -3530,11 +3551,14 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/make-dir": { "version": "4.0.0", @@ -3601,19 +3625,19 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", "dev": true, "license": "MIT", "dependencies": { @@ -3647,6 +3671,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -3663,6 +3697,67 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3677,6 +3772,34 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3693,6 +3816,43 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3737,16 +3897,6 @@ "semver": "bin/semver" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -4250,17 +4400,17 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4286,19 +4436,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/pidtree": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", @@ -4391,16 +4528,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/reflect.getprototypeof": { @@ -4687,6 +4825,7 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4858,18 +4997,17 @@ } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4901,13 +5039,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5089,9 +5220,9 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -5103,18 +5234,34 @@ } }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" + "glob": "^13.0.6", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=18" + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tinyglobby": { @@ -5165,19 +5312,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/ts-api-utils": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", @@ -5519,18 +5653,18 @@ "license": "Apache-2.0" }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -5565,13 +5699,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5613,6 +5740,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5624,22 +5776,21 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { @@ -5668,49 +5819,14 @@ "node": ">=10" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "license": "ISC", "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yocto-queue": { diff --git a/extension/livereview/package.json b/extension/livereview/package.json index 800b331a3..4ef2035a4 100644 --- a/extension/livereview/package.json +++ b/extension/livereview/package.json @@ -133,7 +133,7 @@ "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/vscode": "^1.70.0", - "@vscode/test-cli": "^0.0.12", + "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^2.5.2", "esbuild": "^0.28.1", "eslint": "^9.39.1", @@ -161,8 +161,5 @@ "protobufjs": "^7.6.5", "ws": "^8.21.0", "js-yaml@3": "^4.3.2" - }, - "dependencies": { - "shell-quote": "^1.9.0" } } diff --git a/go.mod b/go.mod index a4130e184..34888b769 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/riverqueue/river v0.32.0 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.32.0 - github.com/riverqueue/river/rivertype v0.32.0 github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.34.0 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 @@ -59,13 +58,13 @@ require ( github.com/stretchr/testify v1.12.1 github.com/tmc/langchaingo v0.1.14 github.com/wasilibs/go-pgquery v0.0.0-20260728010200-155ebad2880e - github.com/xuri/excelize/v2 v2.11.0 + github.com/xuri/excelize/v2 v2.11.1-0.20261009041834-958cda5363be github.com/yalue/onnxruntime_go v1.32.1 github.com/yuin/goldmark v1.8.5 github.com/zricethezav/gitleaks/v8 v8.30.1 gocloud.dev v0.46.0 - golang.org/x/crypto v0.57.0 - golang.org/x/text v0.42.0 + golang.org/x/crypto v0.58.0 + golang.org/x/text v0.43.0 golang.org/x/time v0.15.0 google.golang.org/api v0.272.0 ) @@ -201,10 +200,11 @@ require ( github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/richardlehane/mscfb v1.0.7 // indirect + github.com/richardlehane/mscfb v1.0.8 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect github.com/riverqueue/river/riverdriver v0.32.0 // indirect github.com/riverqueue/river/rivershared v0.32.0 // indirect + github.com/riverqueue/river/rivertype v0.32.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect @@ -236,7 +236,7 @@ require ( github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/efp v0.0.2 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -255,11 +255,11 @@ require ( golang.org/x/arch v0.25.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.41.0 // indirect - golang.org/x/net v0.60.0 // indirect + golang.org/x/net v0.61.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/tools v0.49.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/tools v0.51.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect diff --git a/go.sum b/go.sum index 32cbabb16..ae5e3ecf2 100644 --- a/go.sum +++ b/go.sum @@ -472,8 +472,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= -github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/mscfb v1.0.8 h1:UXdg61fxF69/X9yMYuRHAWSrGXIul/UAPivAsUXMme8= +github.com/richardlehane/mscfb v1.0.8/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/riverqueue/river v0.32.0 h1:j15EoFZ4oQWXcCq8NyzWwoi3fdaO8mECTB100NSv9Qw= @@ -596,10 +596,10 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= -github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88= -github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak= +github.com/xuri/efp v0.0.2 h1:74yhBh0nfIWLOrN76wtShYVse94pADGYS0/c86jOp8Q= +github.com/xuri/efp v0.0.2/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.11.1-0.20261009041834-958cda5363be h1:ccMVMaLSf+qBGXpYkjo3o3oVsB7+Lmkz4ZRLDrqvQME= +github.com/xuri/excelize/v2 v2.11.1-0.20261009041834-958cda5363be/go.mod h1:d2KG/tW81cIhSeC3S7wENv3qRnOC3MDcrjDicKuZa6U= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -662,10 +662,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -679,8 +677,8 @@ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= -golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -697,8 +695,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -717,10 +713,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -735,10 +729,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -760,10 +752,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -774,10 +764,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -807,8 +795,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ui/package-lock.json b/ui/package-lock.json index 7475344b9..6c50fe5b2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -44,7 +44,6 @@ "react-vega": "^8.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", - "shell-quote": "^1.9.0", "thinking-orbs": "^0.2.0", "vega": "^6.3.1", "vega-embed": "^7.1.0", @@ -2260,20 +2259,6 @@ "postcss-selector-parser": "^6.0.10" } }, - "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@csstools/postcss-color-function": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-2.2.3.tgz", @@ -2480,20 +2465,6 @@ "postcss-selector-parser": "^6.0.10" } }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@csstools/postcss-logical-float-and-clear": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-1.0.1.tgz", @@ -2752,20 +2723,6 @@ "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@csstools/postcss-stepped-value-functions": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-2.1.1.tgz", @@ -7912,20 +7869,6 @@ "postcss": "^8.4" } }, - "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/css-declaration-sorter": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz", @@ -7978,20 +7921,6 @@ "postcss-selector-parser": "^6.0.10" } }, - "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -11333,6 +11262,31 @@ "node": ">= 6" } }, + "node_modules/http-proxy-middleware": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/http-proxy/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -15444,20 +15398,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-calc": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", @@ -15689,20 +15629,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-dir-pseudo-class": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-7.0.2.tgz", @@ -15723,20 +15649,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-discard-comments": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.6.tgz", @@ -15839,20 +15751,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-focus-within": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-7.0.2.tgz", @@ -15873,20 +15771,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-font-variant": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", @@ -16316,20 +16200,6 @@ "postcss": "^8.2.14" } }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-nesting": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-11.3.0.tgz", @@ -16374,20 +16244,6 @@ "postcss-selector-parser": "^6.0.10" } }, - "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-normalize-charset": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", @@ -16721,20 +16577,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-reduce-initial": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.6.tgz", @@ -16831,20 +16673,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-selector-parser": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", @@ -18626,6 +18454,7 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -19452,20 +19281,6 @@ "node": ">=10" } }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/tailwindcss/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -21308,31 +21123,6 @@ "node": ">= 6" } }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", - "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, "node_modules/webpack-dev-server/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/ui/package.json b/ui/package.json index 638c957ca..ebe3914ef 100644 --- a/ui/package.json +++ b/ui/package.json @@ -118,7 +118,6 @@ "react-vega": "^8.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", - "shell-quote": "^1.9.0", "thinking-orbs": "^0.2.0", "vega": "^6.3.1", "vega-embed": "^7.1.0", @@ -174,6 +173,11 @@ "@opentelemetry/core": "^2.8.0", "dompurify": "^3.4.16", "undici": "^6.29.0", + "compression": "1.8.2", + "postcss-selector-parser": "7.1.6", + "proxy-addr": "2.0.8", + "shell-quote": "1.12.0", + "source-map-js": "1.2.2", "body-parser": "2.3.0" } } From 9f1bb9a333f12ff0531d0847eb6875382a088fe6 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar Date: Sat, 10 Oct 2026 21:38:40 +0530 Subject: [PATCH 24/24] Refactor Slice Bounds and HTML Entity Decoding --- internal/api/chat_conversations_handler.go | 9 +++++---- internal/docindex/docs/.synced-commits.env | 4 ++-- internal/mcpagent/agent.go | 18 ++++++++++-------- .../SummarySlideshow/slideshowParser.js | 15 +++++++++------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/internal/api/chat_conversations_handler.go b/internal/api/chat_conversations_handler.go index d62fc1319..b33daff9a 100644 --- a/internal/api/chat_conversations_handler.go +++ b/internal/api/chat_conversations_handler.go @@ -223,12 +223,13 @@ func (s *Server) GetConversation(c echo.Context) error { } var actionCard *mcpagent.ActionCard - if len(actionCards) > 0 { - actionCard = &actionCards[len(actionCards)-1] + if n := len(actionCards); n > 0 { + last := actionCards[n-1] + actionCard = &last } var suggestedQuestions []mcpagent.SuggestedQuestionCategory - if len(suggestedQuestionsList) > 0 { - suggestedQuestions = suggestedQuestionsList[len(suggestedQuestionsList)-1] + if n := len(suggestedQuestionsList); n > 0 { + suggestedQuestions = suggestedQuestionsList[n-1] } out.Messages = append(out.Messages, ChatMessageOut{ ID: m.ID, diff --git a/internal/docindex/docs/.synced-commits.env b/internal/docindex/docs/.synced-commits.env index c70d1c96b..b8a9278c5 100644 --- a/internal/docindex/docs/.synced-commits.env +++ b/internal/docindex/docs/.synced-commits.env @@ -1,4 +1,4 @@ -GIT_LRC_COMMIT=cb9fce8104d0dac1cd331ac3353f2c4560bd19d2 +GIT_LRC_COMMIT=1112c13df101abe2c0619ade06a652f1441a58f0 GIT_LRC_WIKI_COMMIT=a1bb17c392a46c1f3bf6cfad6396813ccf80a1a9 -HEXMOSHOMEPAGE_DOCS_COMMIT=48581948963bd587ffacddbd4361b60fa62d0f3e +HEXMOSHOMEPAGE_DOCS_COMMIT=49c80fb9bfd1a1416b3d31edc561f8ffcc0644c6 LIVEREVIEW_WIKI_COMMIT=58ee4a1060bf109d889dd2cc969a776fb337d2f7 diff --git a/internal/mcpagent/agent.go b/internal/mcpagent/agent.go index 52e0a3ac3..6ca4545bd 100644 --- a/internal/mcpagent/agent.go +++ b/internal/mcpagent/agent.go @@ -251,12 +251,14 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry if classifyErr != nil && err == nil { category := aiconnectors.CategorizeLLMError(classifyErr) if tpl, ok := LookupErrorTemplate(category); ok { - lastIdx := len(history) - 1 - if lastIdx >= 0 { - if history[lastIdx] == nil { - history[lastIdx] = make(HistoryEntry) + if len(history) > 0 { + lastIdx := len(history) - 1 + lastEntry := history[lastIdx] + if lastEntry == nil { + lastEntry = make(HistoryEntry) + history[lastIdx] = lastEntry } - fallbackText, _ := history[lastIdx]["text"].(string) + fallbackText, _ := lastEntry["text"].(string) // 1. Create the error entry with the Action Card and Debug Logs errorEntry := HistoryEntry{ @@ -275,14 +277,14 @@ func (a *Agent) RunTurnWithArtifacts(ctx context.Context, history []HistoryEntry // 2. Modify the last entry (the fallback guidance) newFallbackText := "However, here is some general product guidance:\n\n---\n\n" + fallbackText - history[lastIdx]["text"] = newFallbackText - history[lastIdx]["content"] = newFallbackText + lastEntry["text"] = newFallbackText + lastEntry["content"] = newFallbackText // 3. Insert the error entry before the fallback guidance entry var newHistory []HistoryEntry newHistory = append(newHistory, history[:lastIdx]...) newHistory = append(newHistory, errorEntry) - newHistory = append(newHistory, history[lastIdx]) + newHistory = append(newHistory, lastEntry) history = newHistory text = tpl.Message + "\n\n" + newFallbackText diff --git a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js index 296dac16b..7fbe8ebea 100644 --- a/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js +++ b/scripts/blast-radius-demo-export/static/components/SummarySlideshow/slideshowParser.js @@ -92,13 +92,16 @@ function countWords(text) { function extractPlainText(html) { const raw = html || ''; const withoutTags = raw.replace(/<[^>]+>/g, ' '); + return withoutTags - .replace(/ /gi, ' ') - .replace(/&/gi, '&') - .replace(/</gi, '<') - .replace(/>/gi, '>') - .replace(/"/gi, '"') - .replace(/'/gi, "'") + .replace(/&[a-z]+;/gi, (match) => { + const entities = { + ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" + }; + return entities[match.toLowerCase()] || match; + }) + .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec)) + .replace(/&#x([0-9a-f]+);/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16))) .replace(/\s+/g, ' ') .trim(); }