From 86819ea176815f12d054e58f5b32f1e8de8e9be5 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:26:07 +0000 Subject: [PATCH 1/6] refactor: route ThemeContext color-scheme through isLightThemeMode Replace the inline `theme === "light" || theme === "flexoki-light"` check in `getColorScheme` with the shared `isLightThemeMode` helper from `shiki-shared.ts`, so the `-light` suffix convention has a single source of truth and stays consistent with the syntax-highlighting call sites that already use it. --- src/browser/contexts/ThemeContext.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/browser/contexts/ThemeContext.tsx b/src/browser/contexts/ThemeContext.tsx index 5e83061b65..fc20eaa609 100644 --- a/src/browser/contexts/ThemeContext.tsx +++ b/src/browser/contexts/ThemeContext.tsx @@ -10,6 +10,7 @@ import React, { } from "react"; import { readPersistedString, usePersistedState } from "@/browser/hooks/usePersistedState"; import { UI_THEME_KEY } from "@/common/constants/storage"; +import { isLightThemeMode } from "@/browser/utils/highlighting/shiki-shared"; export type ThemeMode = "light" | "dark" | "flexoki-light" | "flexoki-dark"; export type ThemePreference = ThemeMode | "auto"; @@ -74,7 +75,8 @@ const FAVICON_BY_SCHEME: Record<"light" | "dark", string> = { /** Map theme mode to CSS color-scheme value */ function getColorScheme(theme: ThemeMode): "light" | "dark" { - return theme === "light" || theme === "flexoki-light" ? "light" : "dark"; + // Reuse the shared `-light` suffix convention so we have one source of truth for the light/dark mapping. + return isLightThemeMode(theme) ? "light" : "dark"; } function applyThemeFavicon(theme: ThemeMode) { From aced1ca36ef19721addab60f7ae51b47a64b9f9b Mon Sep 17 00:00:00 2001 From: mux-bot Date: Fri, 1 May 2026 12:22:54 +0000 Subject: [PATCH 2/6] refactor: drop unused appendSpace literals on skill/model alias suggestions The skill and model alias build callbacks in getSlashCommandSuggestions hardcode the trailing space, so the appendSpace: true property on those SuggestionDefinition literals is dead. Remove it and add a brief comment explaining why we don't propagate appendSpace through these paths so a future reader doesn't assume the field is consulted there. --- src/browser/utils/slashCommands/suggestions.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/browser/utils/slashCommands/suggestions.ts b/src/browser/utils/slashCommands/suggestions.ts index 35d6751886..d43b896c43 100644 --- a/src/browser/utils/slashCommands/suggestions.ts +++ b/src/browser/utils/slashCommands/suggestions.ts @@ -82,12 +82,14 @@ function buildTopLevelSuggestions( return scope; }; + // The skill build callback below hardcodes the trailing space, so we omit + // `appendSpace` here — leaving it set would be a no-op and falsely suggest + // the build path consults it. const skillDefinitions: SuggestionDefinition[] = (context.agentSkills ?? []) .filter((skill) => !SLASH_COMMAND_DEFINITION_MAP.has(skill.name)) .map((skill) => ({ key: skill.name, description: `${skill.description} (${formatScopeLabel(skill.scope)})`, - appendSpace: true, })); const skillSuggestions = filterAndMapSuggestions(skillDefinitions, partial, (definition) => { @@ -100,12 +102,13 @@ function buildTopLevelSuggestions( }; }); - // Model alias one-shot suggestions (e.g., /haiku, /sonnet, /opus+high) + // Model alias one-shot suggestions (e.g., /haiku, /sonnet, /opus+high). + // The build callback below hardcodes the trailing space, so `appendSpace` + // is intentionally omitted here. const modelAliasDefinitions: SuggestionDefinition[] = Object.entries(MODEL_ABBREVIATIONS).map( ([alias, modelId]) => ({ key: alias, description: `Send with ${formatModelDisplayName(modelId.split(":")[1] ?? modelId)} (one message, +level for thinking)`, - appendSpace: true, }) ); From bb2b9c012bc231f6b96182fc58753b8360b006cd Mon Sep 17 00:00:00 2001 From: mux Date: Fri, 1 May 2026 16:23:22 +0000 Subject: [PATCH 3/6] refactor: extract shared ServiceTier type from ServiceTierSchema The OpenAI service-tier literal union ('auto' | 'default' | 'flex' | 'priority') was duplicated in three places: the CLI options interface (src/cli/run.ts), the providerModelFactory cast at the providers.jsonc boundary, and a local OpenAIServiceTier alias in ProvidersSection.tsx. ServiceTierSchema (src/common/config/schemas/providersConfig.ts) already defines this enum as the runtime source of truth, so derive a TypeScript ServiceTier alias via z.infer once and import it at each site. Pure type-only refactor; the emitted JS and the schema remain unchanged. --- src/browser/features/Settings/Sections/ProvidersSection.tsx | 3 ++- src/cli/run.ts | 3 ++- src/common/config/schemas/providersConfig.ts | 1 + src/node/services/providerModelFactory.ts | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/browser/features/Settings/Sections/ProvidersSection.tsx b/src/browser/features/Settings/Sections/ProvidersSection.tsx index b7c140a04c..7d82c7b08e 100644 --- a/src/browser/features/Settings/Sections/ProvidersSection.tsx +++ b/src/browser/features/Settings/Sections/ProvidersSection.tsx @@ -78,6 +78,7 @@ import type { AddCustomOpenAICompatibleProviderInput, ProviderConfigInfo, } from "@/common/orpc/types"; +import type { ServiceTier } from "@/common/config/schemas/providersConfig"; type MuxGatewayLoginStatus = "idle" | "starting" | "waiting" | "success" | "error"; type CodexOauthFlowStatus = "idle" | "starting" | "waiting" | "error"; @@ -85,7 +86,7 @@ type CopilotLoginStatus = "idle" | "starting" | "waiting" | "success" | "error"; const OPENAI_SERVICE_TIER_UNSET = "unset"; -type OpenAIServiceTier = "auto" | "default" | "flex" | "priority"; +type OpenAIServiceTier = ServiceTier; type OpenAIServiceTierSelectValue = typeof OPENAI_SERVICE_TIER_UNSET | OpenAIServiceTier; function isOpenAIServiceTier(value: string): value is OpenAIServiceTier { diff --git a/src/cli/run.ts b/src/cli/run.ts index 8037588351..233c9f0283 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -35,6 +35,7 @@ import { type SendMessageOptions, type WorkspaceChatMessage, } from "../common/orpc/types"; +import type { ServiceTier } from "../common/config/schemas/providersConfig"; import { createDisplayUsage } from "../common/utils/tokens/displayUsage"; import { getTotalCost, @@ -298,7 +299,7 @@ interface CLIOptions { mcpConfig: boolean; experiment: string[]; budget?: number; - serviceTier?: "auto" | "default" | "flex" | "priority"; + serviceTier?: ServiceTier; use1m?: boolean; keepBackgroundProcesses?: boolean; } diff --git a/src/common/config/schemas/providersConfig.ts b/src/common/config/schemas/providersConfig.ts index 8748293e27..949ab787a2 100644 --- a/src/common/config/schemas/providersConfig.ts +++ b/src/common/config/schemas/providersConfig.ts @@ -5,6 +5,7 @@ import { ProviderModelEntrySchema } from "./providerModelEntry"; export const CacheTtlSchema = z.enum(["5m", "1h"]); export const ServiceTierSchema = z.enum(["auto", "default", "flex", "priority"]); +export type ServiceTier = z.infer; export const CodexOauthDefaultAuthSchema = z.enum(["oauth", "apiKey"]); export const BaseProviderConfigSchema = z diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index fea730ba1f..493d3ba97d 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -20,6 +20,7 @@ import { import { parseCodexOauthAuth } from "@/node/utils/codexOauthAuth"; import type { Config, ProviderConfig, ProvidersConfig } from "@/node/config"; import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { ServiceTier } from "@/common/config/schemas/providersConfig"; import type { ExternalSecretResolver } from "@/common/types/secrets"; import { isOpReference } from "@/common/utils/opRef"; import { isProviderDisabledInConfig } from "@/common/utils/providers/isProviderDisabled"; @@ -1279,7 +1280,7 @@ export class ProviderModelFactory { if (configServiceTier && muxProviderOptions.openai?.serviceTier == null) { muxProviderOptions.openai = { ...muxProviderOptions.openai, - serviceTier: configServiceTier as "auto" | "default" | "flex" | "priority", + serviceTier: configServiceTier as ServiceTier, }; } if (configWireFormat === "responses" || configWireFormat === "chatCompletions") { From c347b9411ce5df82e2479de062a353dd04f310b7 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 00:28:07 +0000 Subject: [PATCH 4/6] refactor: extract ResolvedWorkspaceAiSettings type alias in taskService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shape `{ model: string; thinkingLevel?: ThinkingLevel }` (used internally to read per-agent AI settings off partial workspace metadata) was duplicated 7 times across the parameter and return types of `resolveWorkspaceAISettings`, `resolveTaskAISettings`, and `resolveParentAutoResumeOptions`. The new sub-agent defaults split (#3215) made this duplication especially visible because it added a third method copying the same inline shape. Introduce a private `ResolvedWorkspaceAiSettings` interface at module scope and use it everywhere. Pure type-level cleanup — emitted JS, runtime behavior, and the schema-derived `WorkspaceAISettings` type (where `thinkingLevel` is required) are all unchanged. 🤖 _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh`_ --- src/node/services/taskService.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/node/services/taskService.ts b/src/node/services/taskService.ts index 784d3c6081..1c162ae03c 100644 --- a/src/node/services/taskService.ts +++ b/src/node/services/taskService.ts @@ -94,6 +94,17 @@ export type TaskKind = "agent"; export type AgentTaskStatus = NonNullable; +/** + * Resolved per-agent AI settings (canonical model + optional thinking level). + * + * `thinkingLevel` is optional because internal callers read these settings off of + * partial workspace metadata where the field may be missing on older entries. + */ +interface ResolvedWorkspaceAiSettings { + model: string; + thinkingLevel?: ThinkingLevel; +} + export interface AgentTaskStatusLookup { exists: boolean; taskStatus: AgentTaskStatus | null; @@ -383,11 +394,11 @@ export class TaskService { // fall back to legacy workspace settings for older configs. private resolveWorkspaceAISettings( workspace: { - aiSettingsByAgent?: Record; - aiSettings?: { model: string; thinkingLevel?: ThinkingLevel }; + aiSettingsByAgent?: Record; + aiSettings?: ResolvedWorkspaceAiSettings; }, agentId: string | undefined - ): { model: string; thinkingLevel?: ThinkingLevel } | undefined { + ): ResolvedWorkspaceAiSettings | undefined { const normalizedAgentId = typeof agentId === "string" && agentId.trim().length > 0 ? normalizeAgentId(agentId, "") @@ -401,8 +412,8 @@ export class TaskService { private resolveTaskAISettings(params: { cfg: ReturnType; parentMeta: { - aiSettingsByAgent?: Record; - aiSettings?: { model: string; thinkingLevel?: ThinkingLevel }; + aiSettingsByAgent?: Record; + aiSettings?: ResolvedWorkspaceAiSettings; }; agentId: string; modelString?: string; @@ -451,8 +462,8 @@ export class TaskService { parentWorkspaceId: string, parentEntry: { workspace: { - aiSettingsByAgent?: Record; - aiSettings?: { model: string; thinkingLevel?: ThinkingLevel }; + aiSettingsByAgent?: Record; + aiSettings?: ResolvedWorkspaceAiSettings; }; }, fallbackModel: string, From 467fc244ffaffcc7194af10eaf5a89cceff7626c Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 05:04:35 +0000 Subject: [PATCH 5/6] refactor: drop redundant GuardAnchors type alias in file_edit_insert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In `src/node/services/tools/file_edit_insert.ts`, `GuardAnchors` was defined as `Pick`, but `InsertContentOptions` itself is already `Pick` after the `.nullish()` strict-mode refactor in #2250 stripped the `InsertContentOptions` interface down to those same two fields. The two aliases are now structurally identical, so `GuardAnchors` is dead. Drop the alias and use `InsertContentOptions` for the two callers (`insertWithGuards`, `resolveGuardAnchor`). Both names were file-local; no exports change. The function names (`insertWithGuards`, `resolveGuardAnchor`) already convey "guard" context, so the parameter type doesn't need to repeat it. Pure type-level cleanup — emitted JS, runtime behavior, and the public tool surface are all unchanged. --- src/node/services/tools/file_edit_insert.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/services/tools/file_edit_insert.ts b/src/node/services/tools/file_edit_insert.ts index fd0354b2b2..12a7f6a6f6 100644 --- a/src/node/services/tools/file_edit_insert.ts +++ b/src/node/services/tools/file_edit_insert.ts @@ -44,8 +44,6 @@ function guardFailure(error: string): InsertOperationFailure { }; } -type GuardAnchors = Pick; - export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration) => { return tool({ description: TOOL_DEFINITIONS.file_edit_insert.description, @@ -175,7 +173,7 @@ function insertContent( function insertWithGuards( originalContent: string, contentToInsert: string, - anchors: GuardAnchors + anchors: InsertContentOptions ): InsertOperationSuccess | InsertOperationFailure { const anchorResult = resolveGuardAnchor(originalContent, anchors); if (!anchorResult.success) { @@ -216,7 +214,7 @@ function findUniqueSubstringIndex( function resolveGuardAnchor( originalContent: string, - { insert_before, insert_after }: GuardAnchors + { insert_before, insert_after }: InsertContentOptions ): GuardResolutionSuccess | InsertOperationFailure { const fileEol = detectFileEol(originalContent); From 34c8ebcf087818ccec4d492fa3feb4ae05b72023 Mon Sep 17 00:00:00 2001 From: ammar-agent Date: Sat, 2 May 2026 20:18:11 +0000 Subject: [PATCH 6/6] refactor: extract pushStreamErrorRow helper in StreamingMessageAggregator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both stream-error rows in `buildDisplayedMessagesForMessage` (the existing `message.metadata?.error` branch and the new `finishReason === "length"` branch added in #3223) push structurally identical objects, differing only in `id` suffix, `error` string, and `errorType`. The shared parent-message-derived fields (`historyId`, `historySequence`, `model`, `routedThroughGateway`, `timestamp`) were duplicated across both pushes. Extract a local `pushStreamErrorRow` closure that captures the shared fields once. Each branch now reduces to a single call passing the three differing values. Pure refactor — emitted DisplayedMessage objects are identical. --- .../messages/StreamingMessageAggregator.ts | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/browser/utils/messages/StreamingMessageAggregator.ts b/src/browser/utils/messages/StreamingMessageAggregator.ts index 76987e5bb6..655e924e4f 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.ts @@ -27,6 +27,7 @@ import { type StreamLifecycleSnapshot, } from "@/common/types/stream"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; +import type { StreamErrorType } from "@/common/types/errors"; import type { TodoItem, StatusSetToolResult, NotifyToolResult } from "@/common/types/tools"; import { completeInProgressTodoItems } from "@/common/utils/todoList"; import { getToolOutputUiOnly } from "@/common/utils/tools/toolOutputUiOnly"; @@ -3064,20 +3065,37 @@ export class StreamingMessageAggregator { } }); - // Create stream-error DisplayedMessage if message has error metadata - // This happens after all parts are displayed, so error appears at the end - if (message.metadata?.error) { + // Both stream-error rows (real error metadata + synthesized + // max_tokens truncation) share the same parent-message-derived + // fields. Capture them in one place so adding a new branch later + // can't accidentally drift on `model` / `routedThroughGateway` / + // `historySequence` / `timestamp`. + const pushStreamErrorRow = ( + idSuffix: string, + error: string, + errorType: StreamErrorType + ): void => { displayedMessages.push({ type: "stream-error", - id: `${message.id}-error`, + id: `${message.id}-${idSuffix}`, historyId: message.id, - error: message.metadata.error, - errorType: message.metadata.errorType ?? "unknown", + error, + errorType, historySequence, - model: message.metadata.model, + model: message.metadata?.model, routedThroughGateway: message.metadata?.routedThroughGateway, timestamp: baseTimestamp, }); + }; + + // Create stream-error DisplayedMessage if message has error metadata + // This happens after all parts are displayed, so error appears at the end + if (message.metadata?.error) { + pushStreamErrorRow( + "error", + message.metadata.error, + message.metadata.errorType ?? "unknown" + ); } else if ( // Stream ended cleanly *but* the provider truncated us at max_tokens. // The backend's stream-end path treats this as a successful completion @@ -3090,19 +3108,12 @@ export class StreamingMessageAggregator { !hasActiveStream && message.metadata?.finishReason === "length" ) { - displayedMessages.push({ - type: "stream-error", - id: `${message.id}-length`, - historyId: message.id, - error: - "The model hit its max output token limit before finishing this response. " + + pushStreamErrorRow( + "length", + "The model hit its max output token limit before finishing this response. " + "Lower the thinking level (or split the turn into smaller steps) to give it more headroom.", - errorType: "max_output_tokens", - historySequence, - model: message.metadata.model, - routedThroughGateway: message.metadata?.routedThroughGateway, - timestamp: baseTimestamp, - }); + "max_output_tokens" + ); } }