Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/browser/contexts/ThemeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/browser/features/Settings/Sections/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,15 @@ 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";
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 {
Expand Down
49 changes: 30 additions & 19 deletions src/browser/utils/messages/StreamingMessageAggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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"
);
}
}

Expand Down
9 changes: 6 additions & 3 deletions src/browser/utils/slashCommands/suggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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,
})
);

Expand Down
3 changes: 2 additions & 1 deletion src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -298,7 +299,7 @@ interface CLIOptions {
mcpConfig: boolean;
experiment: string[];
budget?: number;
serviceTier?: "auto" | "default" | "flex" | "priority";
serviceTier?: ServiceTier;
use1m?: boolean;
keepBackgroundProcesses?: boolean;
}
Expand Down
1 change: 1 addition & 0 deletions src/common/config/schemas/providersConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ServiceTierSchema>;
export const CodexOauthDefaultAuthSchema = z.enum(["oauth", "apiKey"]);

export const BaseProviderConfigSchema = z
Expand Down
3 changes: 2 additions & 1 deletion src/node/services/providerModelFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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") {
Expand Down
25 changes: 18 additions & 7 deletions src/node/services/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ export type TaskKind = "agent";

export type AgentTaskStatus = NonNullable<WorkspaceConfigEntry["taskStatus"]>;

/**
* 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;
Expand Down Expand Up @@ -383,11 +394,11 @@ export class TaskService {
// fall back to legacy workspace settings for older configs.
private resolveWorkspaceAISettings(
workspace: {
aiSettingsByAgent?: Record<string, { model: string; thinkingLevel?: ThinkingLevel }>;
aiSettings?: { model: string; thinkingLevel?: ThinkingLevel };
aiSettingsByAgent?: Record<string, ResolvedWorkspaceAiSettings>;
aiSettings?: ResolvedWorkspaceAiSettings;
},
agentId: string | undefined
): { model: string; thinkingLevel?: ThinkingLevel } | undefined {
): ResolvedWorkspaceAiSettings | undefined {
const normalizedAgentId =
typeof agentId === "string" && agentId.trim().length > 0
? normalizeAgentId(agentId, "")
Expand All @@ -401,8 +412,8 @@ export class TaskService {
private resolveTaskAISettings(params: {
cfg: ReturnType<Config["loadConfigOrDefault"]>;
parentMeta: {
aiSettingsByAgent?: Record<string, { model: string; thinkingLevel?: ThinkingLevel }>;
aiSettings?: { model: string; thinkingLevel?: ThinkingLevel };
aiSettingsByAgent?: Record<string, ResolvedWorkspaceAiSettings>;
aiSettings?: ResolvedWorkspaceAiSettings;
};
agentId: string;
modelString?: string;
Expand Down Expand Up @@ -451,8 +462,8 @@ export class TaskService {
parentWorkspaceId: string,
parentEntry: {
workspace: {
aiSettingsByAgent?: Record<string, { model: string; thinkingLevel?: ThinkingLevel }>;
aiSettings?: { model: string; thinkingLevel?: ThinkingLevel };
aiSettingsByAgent?: Record<string, ResolvedWorkspaceAiSettings>;
aiSettings?: ResolvedWorkspaceAiSettings;
};
},
fallbackModel: string,
Expand Down
6 changes: 2 additions & 4 deletions src/node/services/tools/file_edit_insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ function guardFailure(error: string): InsertOperationFailure {
};
}

type GuardAnchors = Pick<InsertContentOptions, "insert_before" | "insert_after">;

export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration) => {
return tool({
description: TOOL_DEFINITIONS.file_edit_insert.description,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down
Loading