Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dd99292
πŸ€– refactor: dedupe memory sweep recordUsage callbacks
mux-bot[bot] Jul 8, 2026
9a6e902
πŸ€– refactor: dedupe memory scope-full cap check into helper
mux-bot[bot] Jul 9, 2026
6154de6
refactor: dedupe blockquote line formatting in bash monitor wake prompt
mux-bot[bot] Jul 9, 2026
eb20196
refactor: dedupe tool_search removal in prepareToolSearch
mux-bot[bot] Jul 9, 2026
12e691b
refactor: dedupe anthropic cache-create token extraction in usageHelpers
mux-bot[bot] Jul 10, 2026
3f865b9
refactor: dedupe capability-model thinking policy resolution
mux-bot[bot] Jul 10, 2026
fe63330
refactor: dedupe queue entry clear-callback projection in MessageQueue
mux-bot[bot] Jul 11, 2026
58d7795
refactor: dedupe OpenAI-origin model check in cacheStrategy
mux-bot[bot] Jul 12, 2026
747c54a
refactor: dedupe tool-call-execution-start emit in StreamManager
mux-bot[bot] Jul 13, 2026
f2ddbaa
refactor: dedupe model-parameter extras merge in aiService
mux-bot[bot] Jul 13, 2026
45b1da1
refactor: unify legacy tool_search part rename helper in toolCatalog
mux-bot[bot] Jul 14, 2026
a386c40
refactor: drop duplicated context-cap rationale comment in codexOAuth
mux-bot[bot] Jul 14, 2026
5150732
refactor: dedupe flat-section pinned block resolution in pinnedReorder
mux-bot[bot] Jul 15, 2026
629dc6a
refactor: dedupe JSON-wrapped tool-output unwrap in workflowRunMessages
mux-bot[bot] Jul 15, 2026
15b6b3d
refactor: hoist errorType local in finalizeWorkspaceTurnFromStreamError
mux-bot[bot] Jul 15, 2026
eb81423
refactor: extract buildSkillDescriptor helper for skill discovery
mux-bot[bot] Jul 15, 2026
0de6b00
refactor: hoist duplicated Date.parse(createdAt) in bash monitor deli…
mux-bot[bot] Jul 16, 2026
e7b6c3e
refactor: extract awaitPendingLoad helper in DevToolsService
mux-bot[bot] Jul 16, 2026
7fc730e
refactor: dedupe MCP OAuth redirect URI resolution in router
mux-bot[bot] Jul 18, 2026
ded4a3e
refactor: extract getTotalTokens helper for total-token sums
mux-bot[bot] Jul 20, 2026
f0451f9
refactor: hoist duplicated dedupeKeys snapshot in removeByDedupeKeyPr…
mux-bot[bot] Jul 20, 2026
ad1a3f3
refactor: dedupe settled workspace-turn reconciliation guard
mux-bot[bot] Jul 21, 2026
69a5aa6
refactor: dedupe fire-and-forget archive-all catch in TaskGroupListItem
mux-bot[bot] Jul 21, 2026
d31fd7e
refactor: extract someDescendantAgentTaskWorkspace helper for sticky-…
mux-bot[bot] Jul 21, 2026
7963b5f
refactor: drop duplicated Kimi K3 max-effort rationale in providerOpt…
mux-bot[bot] Jul 22, 2026
f1ddd64
refactor: drop redundant structuredOutput guard at subagent report ca…
mux-bot[bot] Jul 22, 2026
3fabc97
refactor: extract isZipMediaType helper for staged attachment media-t…
mux-bot[bot] Jul 23, 2026
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
14 changes: 8 additions & 6 deletions src/browser/components/ProjectSidebar/TaskGroupListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ function getAggregateVisualState(props: TaskGroupListItemProps): VisualState {

export function TaskGroupListItem(props: TaskGroupListItemProps) {
const contextMenu = useContextMenuPosition();
// Variant-group archive is fire-and-forget from the row.
const archiveAll = (buttonElement: HTMLElement) => {
props.onArchiveAll?.(buttonElement).catch(() => {
// The sidebar owner surfaces archive failures through its shared error UI.
});
};
const hasRunningWork = props.runningCount > 0;
const aggregateState = getAggregateVisualState(props);
const statusDescriptionId = `task-group-status-${props.groupId}`;
Expand Down Expand Up @@ -104,9 +110,7 @@ export function TaskGroupListItem(props: TaskGroupListItemProps) {
if (props.onArchiveAll && matchesKeybind(event, KEYBINDS.ARCHIVE_WORKSPACE)) {
event.preventDefault();
stopKeyboardPropagation(event);
props.onArchiveAll(event.currentTarget).catch(() => {
// The sidebar owner surfaces archive failures through its shared error UI.
});
archiveAll(event.currentTarget);
return;
}
if (event.target !== event.currentTarget) {
Expand Down Expand Up @@ -191,9 +195,7 @@ export function TaskGroupListItem(props: TaskGroupListItemProps) {
variant="destructive"
onClick={(event) => {
contextMenu.close();
props.onArchiveAll?.(event.currentTarget).catch(() => {
// The sidebar owner surfaces archive failures through its shared error UI.
});
archiveAll(event.currentTarget);
}}
/>
</PositionedMenu>
Expand Down
8 changes: 2 additions & 6 deletions src/browser/features/RightSidebar/CostsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
sumUsageHistory,
formatCostWithDollar,
getTotalCost,
getTotalTokens,
type ChatUsageDisplay,
} from "@/common/utils/tokens/usageAggregator";
import { normalizeToCanonical, formatModelStringForDisplay } from "@/common/utils/ai/models";
Expand Down Expand Up @@ -54,12 +55,7 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
return Array.from(merged.entries())
.map(([model, entry]) => ({
model,
tokens:
entry.input.tokens +
entry.cached.tokens +
entry.cacheCreate.tokens +
entry.output.tokens +
entry.reasoning.tokens,
tokens: getTotalTokens(entry),
cost: getTotalCost(entry),
}))
.sort((a, b) => (b.cost ?? 0) - (a.cost ?? 0) || b.tokens - a.tokens);
Expand Down
10 changes: 2 additions & 8 deletions src/browser/stores/WorkspaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import { isDurableCompactionBoundaryMarker } from "@/common/utils/messages/compa
import { isSideQuestionAnswerMessage as isSideQuestionAnswerMuxMessage } from "@/common/utils/messages/sideQuestion";
import { WorkspaceConsumerManager } from "./WorkspaceConsumerManager";
import type { ChatUsageDisplay } from "@/common/utils/tokens/usageAggregator";
import { sumUsageHistory } from "@/common/utils/tokens/usageAggregator";
import { sumUsageHistory, getTotalTokens } from "@/common/utils/tokens/usageAggregator";
import type { TokenConsumer } from "@/common/types/chatStats";
import { normalizeToCanonical } from "@/common/utils/ai/models";
import type { z } from "zod";
Expand Down Expand Up @@ -2455,13 +2455,7 @@ export class WorkspaceStore {
const lastRequest = sessionData?.lastRequest;

// Calculate total tokens from session total
const totalTokens = sessionTotal
? sessionTotal.input.tokens +
sessionTotal.cached.tokens +
sessionTotal.cacheCreate.tokens +
sessionTotal.output.tokens +
sessionTotal.reasoning.tokens
: 0;
const totalTokens = getTotalTokens(sessionTotal);

const messages = aggregator.getAllMessages();
if (messages.length === 0) {
Expand Down
33 changes: 22 additions & 11 deletions src/browser/utils/ui/pinnedReorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ function collectFlatSectionRows(
return orderMultiProjectSectionRows(Array.from(byId.values()));
}

/**
* Resolve the pinned block for a flat-rendered section (multi-project or
* scratch). Both render as a single block regardless of source bucket, so the
* block's `fullOrder` and `blockIds` are identical: every pinned id of the
* section, in rendered order. Returns null when `meta` is not one of them.
*/
function locateFlatSectionPinnedBlock(
meta: FrontendWorkspaceMetadata,
sortedWorkspacesByProject: Map<string, FrontendWorkspaceMetadata[]>,
includeRow: (row: FrontendWorkspaceMetadata) => boolean
): PinnedBlock | null {
const pinnedIds = collectFlatSectionRows(sortedWorkspacesByProject, includeRow)
.filter(isWorkspacePinned)
.map((row) => row.id);
if (!pinnedIds.includes(meta.id)) return null;
return { fullOrder: pinnedIds, blockIds: pinnedIds };
}

/**
* Resolve the pinned block containing `meta`, mirroring the sidebar renderer:
* multi-project rows form one flat block; regular rows partition by their
Expand All @@ -65,22 +83,15 @@ export function locatePinnedBlock(
// partitioning below would isolate every row into a block of one and
// swallow reorders. Treat them like the multi-project section instead.
if (meta.kind === "scratch") {
const pinnedIds = collectFlatSectionRows(
return locateFlatSectionPinnedBlock(
meta,
sortedWorkspacesByProject,
(row) => row.kind === "scratch"
)
.filter(isWorkspacePinned)
.map((row) => row.id);
if (!pinnedIds.includes(meta.id)) return null;
return { fullOrder: pinnedIds, blockIds: pinnedIds };
);
}

if (isMultiProject(meta)) {
const pinnedIds = collectFlatSectionRows(sortedWorkspacesByProject, isMultiProject)
.filter(isWorkspacePinned)
.map((row) => row.id);
if (!pinnedIds.includes(meta.id)) return null;
return { fullOrder: pinnedIds, blockIds: pinnedIds };
return locateFlatSectionPinnedBlock(meta, sortedWorkspacesByProject, isMultiProject);
}

const rows = sortedWorkspacesByProject.get(meta.projectPath) ?? [];
Expand Down
19 changes: 3 additions & 16 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type { ServiceTier } from "../common/config/schemas/providersConfig";
import { createDisplayUsage } from "../common/utils/tokens/displayUsage";
import {
getTotalCost,
getTotalTokens,
formatCostWithDollar,
sumUsageHistory,
type ChatUsageDisplay,
Expand Down Expand Up @@ -1163,14 +1164,7 @@ async function main(): Promise<number> {
if (budget !== undefined && !budgetExceeded) {
const totalUsage = sumUsageHistory(usageHistory);
const cost = getTotalCost(totalUsage);
const hasTokens = totalUsage
? totalUsage.input.tokens +
totalUsage.output.tokens +
totalUsage.cached.tokens +
totalUsage.cacheCreate.tokens +
totalUsage.reasoning.tokens >
0
: false;
const hasTokens = getTotalTokens(totalUsage) > 0;

if (hasTokens && cost === undefined) {
const errMsg = `Cannot enforce budget: unknown pricing for model "${payload.metadata.model ?? model}"`;
Expand Down Expand Up @@ -1253,14 +1247,7 @@ async function main(): Promise<number> {
// Reject if model has unknown pricing: displayUsage exists with tokens but cost is undefined
// (createDisplayUsage doesn't set hasUnknownCosts; that's only set by sumUsageHistory)
// Include all token types: input, output, cached, cacheCreate, and reasoning
const hasTokens =
displayUsage &&
displayUsage.input.tokens +
displayUsage.output.tokens +
displayUsage.cached.tokens +
displayUsage.cacheCreate.tokens +
displayUsage.reasoning.tokens >
0;
const hasTokens = getTotalTokens(displayUsage) > 0;
if (hasTokens && cost === undefined) {
const errMsg = `Cannot enforce budget: unknown pricing for model "${model}"`;
emitJsonLine({ type: "budget-error", error: errMsg, model });
Expand Down
1 change: 0 additions & 1 deletion src/common/constants/codexOAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export const CODEX_OAUTH_REQUIRED_MODELS = new Set<string>([
const CODEX_OAUTH_CONTEXT_WINDOW_OVERRIDES: Record<string, number> = {
// The public API exposes a 1.05M window for these models, but the ChatGPT/Codex
// model catalog publishes smaller context windows (372K for the GPT-5.6 family).
// Keep auth-route caps separate so API-key requests retain the full public window.
"gpt-5.5": 272_000,
"gpt-5.6": 372_000,
"gpt-5.6-sol": 372_000,
Expand Down
1 change: 1 addition & 0 deletions src/common/orpc/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
AgentSkillPackageSchema,
AgentSkillScopeSchema,
SkillNameSchema,
buildSkillDescriptor,
resolveSkillAdvertise,
resolveSkillUserInvocable,
resolveSkillWhenToUse,
Expand Down
23 changes: 23 additions & 0 deletions src/common/orpc/schemas/agentSkill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@ export const AgentSkillDescriptorSchema = z.object({
whenToUse: z.string().min(1).max(1024).optional(),
});

/**
* Map validated SKILL.md frontmatter to the normalized AgentSkillDescriptor shape.
*
* Centralizes the frontmatter→descriptor field mapping (advertise / user-invocable /
* argument-hint / when-to-use normalization) so every discovery path produces byte-identical
* descriptors. Callers still validate the result with AgentSkillDescriptorSchema, since they
* handle validation failures differently (tool listing vs. diagnostics-collecting discovery).
*/
export function buildSkillDescriptor(
frontmatter: z.infer<typeof AgentSkillFrontmatterSchema>,
scope: z.infer<typeof AgentSkillScopeSchema>
): z.infer<typeof AgentSkillDescriptorSchema> {
return {
name: frontmatter.name,
description: frontmatter.description,
scope,
advertise: resolveSkillAdvertise(frontmatter),
userInvocable: resolveSkillUserInvocable(frontmatter),
argumentHint: frontmatter["argument-hint"],
whenToUse: resolveSkillWhenToUse(frontmatter),
};
}

export const AgentSkillPackageSchema = z
.object({
scope: AgentSkillScopeSchema,
Expand Down
16 changes: 12 additions & 4 deletions src/common/utils/ai/cacheStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ function isOfficialOpenAIBaseUrl(baseUrl: string): boolean {
);
}

/**
* Whether a canonical `provider:model` string has an `openai` origin with a
* non-empty model name. Used to gate both the request model and its resolved
* capability target in openaiExplicitPromptCachingAvailable.
*/
function isOpenAIOriginModel(canonical: string): boolean {
const [origin, modelName] = canonical.split(":", 2);
return origin === "openai" && !!modelName;
}

/**
* Route-aware eligibility for GPT-5.6 explicit prompt cache breakpoints.
*
Expand Down Expand Up @@ -200,16 +210,14 @@ export function openaiExplicitPromptCachingAvailable(
}

const normalized = normalizeToCanonical(modelString);
const [origin, modelName] = normalized.split(":", 2);
if (origin !== "openai" || !modelName) {
if (!isOpenAIOriginModel(normalized)) {
return false;
}

// Mapped aliases inherit eligibility only when the resolved capability
// target is also an OpenAI GPT-5.6-family model.
const capabilityModel = resolveModelForMetadata(normalized, providersConfig);
const [capabilityOrigin, capabilityModelName] = capabilityModel.split(":", 2);
if (capabilityOrigin !== "openai" || !capabilityModelName) {
if (!isOpenAIOriginModel(capabilityModel)) {
return false;
}
if (!isGpt56FamilyModel(capabilityModel)) {
Expand Down
10 changes: 5 additions & 5 deletions src/common/utils/ai/providerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,8 @@ export function buildProviderOptions(

// Build Moonshot-specific options
if (formatProvider === "moonshotai") {
// Kimi K3 always reasons and supports only the max reasoning effort. Send it
// explicitly rather than relying on the API default.
// Send the max reasoning effort explicitly rather than relying on the API
// default (see isKimiK3Model for why K3 only accepts "max").
if (isKimiK3Model(capabilityModel)) {
const options = {
moonshotai: { reasoningEffort: "max" },
Expand All @@ -501,9 +501,9 @@ export function buildProviderOptions(

// Build OpenRouter-specific options
if (formatProvider === "openrouter") {
// Kimi K3 always reasons and supports only the max reasoning effort. Send it
// explicitly: `enabled: true` alone falls back to OpenRouter's default (medium)
// effort, which the model does not support.
// Send the max reasoning effort explicitly: `enabled: true` alone falls back
// to OpenRouter's default (medium) effort, which K3 does not support (see
// isKimiK3Model for why K3 only accepts "max").
const reasoningEffort = isKimiK3Model(capabilityModel)
? "max"
: OPENROUTER_REASONING_EFFORT[effectiveThinking];
Expand Down
13 changes: 8 additions & 5 deletions src/common/utils/attachments/supportedAttachmentMediaTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export function getSupportedAttachmentMediaType(args: {
return isSupportedAttachmentMediaType(normalized) ? normalized : null;
}

// Membership check against the accepted ZIP media types, isolating the
// `as const` tuple cast that both staged-attachment paths would otherwise repeat.
function isZipMediaType(normalized: string): boolean {
return ZIP_MEDIA_TYPES.includes(normalized as (typeof ZIP_MEDIA_TYPES)[number]);
}

function sanitizeStagedAttachmentMediaType(mediaType: string): string | null {
const normalized = normalizeAttachmentMediaType(mediaType);
if (
Expand All @@ -79,10 +85,7 @@ function sanitizeStagedAttachmentMediaType(mediaType: string): string | null {

export function isSupportedStagedAttachmentMediaType(mediaType: string): boolean {
const normalized = normalizeAttachmentMediaType(mediaType);
return (
ZIP_MEDIA_TYPES.includes(normalized as (typeof ZIP_MEDIA_TYPES)[number]) ||
sanitizeStagedAttachmentMediaType(normalized) != null
);
return isZipMediaType(normalized) || sanitizeStagedAttachmentMediaType(normalized) != null;
}

export function getSupportedStagedAttachmentMediaType(args: {
Expand All @@ -92,7 +95,7 @@ export function getSupportedStagedAttachmentMediaType(args: {
const trimmedMediaType = args.mediaType?.trim();
if (trimmedMediaType != null && trimmedMediaType.length > 0) {
const normalized = normalizeAttachmentMediaType(trimmedMediaType);
if (ZIP_MEDIA_TYPES.includes(normalized as (typeof ZIP_MEDIA_TYPES)[number])) {
if (isZipMediaType(normalized)) {
return ZIP_MEDIA_TYPE;
}
return sanitizeStagedAttachmentMediaType(normalized) ?? DEFAULT_STAGED_MEDIA_TYPE;
Expand Down
22 changes: 16 additions & 6 deletions src/common/utils/thinking/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ export function getThinkingPolicyForModel(
modelString: string,
providersConfig?: ProvidersConfigMap | null
): ThinkingPolicy {
const capabilityModel = resolveModelForMetadata(modelString, providersConfig ?? null);
return getExplicitThinkingPolicy(capabilityModel) ?? DEFAULT_THINKING_POLICY;
return getExplicitThinkingPolicyForModel(modelString, providersConfig) ?? DEFAULT_THINKING_POLICY;
}

/**
Expand Down Expand Up @@ -181,6 +180,20 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null {
return null;
}

/**
* Resolve a model to its capability model (following `mappedToModel` aliases via
* {@link resolveModelForMetadata}) and return its explicit reasoning policy, or
* `null` when none matches. Shared by {@link getThinkingPolicyForModel} and
* {@link hasExplicitThinkingPolicy}, which must resolve aliases identically
* before rule matching so a mapped alias inherits its target's policy.
*/
function getExplicitThinkingPolicyForModel(
modelString: string,
providersConfig?: ProvidersConfigMap | null
): ThinkingPolicy | null {
return getExplicitThinkingPolicy(resolveModelForMetadata(modelString, providersConfig ?? null));
}

/** Canonical ordering index for a level (off=0 … max=5). */
function thinkingLevelIndex(level: ThinkingLevel): number {
return THINKING_LEVELS.indexOf(level);
Expand Down Expand Up @@ -222,10 +235,7 @@ export function hasExplicitThinkingPolicy(
modelString: string,
providersConfig?: ProvidersConfigMap | null
): boolean {
return (
getExplicitThinkingPolicy(resolveModelForMetadata(modelString, providersConfig ?? null)) !==
null
);
return getExplicitThinkingPolicyForModel(modelString, providersConfig) !== null;
}

/**
Expand Down
9 changes: 2 additions & 7 deletions src/common/utils/tokens/tokenMeterUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ProvidersConfigMap } from "@/common/orpc/types";
import { getEffectiveContextLimit } from "@/common/utils/compaction/contextLimit";
import type { ChatUsageDisplay } from "./usageAggregator";
import { getTotalTokens, type ChatUsageDisplay } from "./usageAggregator";

// NOTE: Provide theme-matching fallbacks so token meters render consistently
// even if a host environment doesn't define the CSS variables (e.g., an embedded UI).
Expand Down Expand Up @@ -69,12 +69,7 @@ export function calculateTokenMeterData(
// Total tokens used in the request.
// For Anthropic prompt caching, cacheCreate tokens are reported separately but still
// count toward total input tokens for the request.
const totalUsed =
usage.input.tokens +
usage.cached.tokens +
usage.cacheCreate.tokens +
usage.output.tokens +
usage.reasoning.tokens;
const totalUsed = getTotalTokens(usage);

const toPercentage = (tokens: number) => {
if (verticalProportions) {
Expand Down
Loading
Loading