Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a90720d
[openai] 🤖 feat: add project-specific Codex OAuth accounts
Sep 6, 2026
7af33f3
[openai] 🤖 fix: address account routing and persistence reviews
Sep 6, 2026
0219f20
[openai] 🤖 fix: preserve account identity across processes
Sep 6, 2026
5262cba
[openai] 🤖 fix: preserve reconnect identity after rejected refreshes
Sep 6, 2026
8a6982f
[openai] 🤖 fix: complete account lifecycle and keyboard controls
Sep 6, 2026
63d3dda
[openai] 🤖 fix: align disconnect and compaction account state
Sep 6, 2026
8de6c6f
[openai] 🤖 refactor: let React Compiler manage account callbacks
Sep 6, 2026
c737507
[openai] 🤖 fix: exclude revoked credentials from route availability
Sep 6, 2026
fae7c69
[openai] 🤖 fix: pin compaction credentials and honor model availability
Sep 6, 2026
bb14413
[openai] 🤖 fix: verify project account writes and recover credential IDs
Sep 6, 2026
6aa0653
[openai] 🤖 fix: preserve legacy requests during failed reconnect startup
Sep 6, 2026
0875908
[openai] 🤖 fix: preserve account identity across startup and retry races
Sep 6, 2026
477023f
[openai] 🤖 tests: update workspace routing snapshot mocks
Sep 6, 2026
d1b00a4
[openai] 🤖 tests: preserve routing checks after cost tracking changes
Sep 6, 2026
e5664c3
[openai] 🤖 fix: preserve compaction accounts and allow API-key recovery
Sep 6, 2026
d7869b5
[openai] 🤖 fix: preserve account identity and recovery across automat…
Sep 6, 2026
e5b47cd
[openai] 🤖 fix: retain accepted account snapshots in nested tools
Sep 6, 2026
5076fc7
[openai] 🤖 fix: preserve accepted routes and live context limits
Sep 6, 2026
008d2d8
[openai] 🤖 fix: preserve gateway limits and workflow continuation acc…
Sep 6, 2026
dbf0bad
[openai] 🤖 fix: protect named OAuth credentials after downgrade
Sep 6, 2026
02b47f8
[openai] 🤖 fix: pin summary routing and initial context limits
Sep 6, 2026
35b6e37
[openai] 🤖 fix: preserve legacy requests during canceled reconnects
Sep 6, 2026
7ba64e4
[openai] 🤖 fix: preserve project aliases and fallback metadata
Sep 6, 2026
55f5a28
[openai] 🤖 tests: align fallback event expectations
Sep 6, 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
27 changes: 26 additions & 1 deletion src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLocation, useNavigate } from "react-router-dom";
import "./styles/globals.css";
import { useWorkspaceContext, toWorkspaceSelection } from "./contexts/WorkspaceContext";
import { useProjectContext } from "./contexts/ProjectContext";
import { getCodexOauthProjectPath } from "@/common/utils/providers/codexOauthRouting";
import type { WorkspaceSelection } from "./components/ProjectSidebar/ProjectSidebar";
import { LeftSidebar } from "./components/LeftSidebar/LeftSidebar";
import { ProjectCreateModal } from "./components/ProjectCreateModal/ProjectCreateModal";
Expand Down Expand Up @@ -197,6 +198,7 @@ function AppInner() {

const {
userProjects,
getProjectConfig,
refreshProjects,
removeProject,
openProjectCreateModal,
Expand Down Expand Up @@ -263,6 +265,18 @@ function AppInner() {
)
: null;
const creationScopeId = creationScope ? getProjectScopeId(creationScope.projectPath) : null;
const accountWorkspaceId = selectedWorkspace?.workspaceId ?? currentWorkspaceId;
const accountProjectPath = getCodexOauthProjectPath(
accountWorkspaceId
? (workspaceMetadata.get(accountWorkspaceId) ?? selectedWorkspace)
: {
projectPath: creationScope?.projectPath,
subProjectPath: creationScope?.subProjectPath ?? undefined,
}
);
const codexOauthAccountId = accountProjectPath
? getProjectConfig(accountProjectPath)?.codexOauthAccountId
: undefined;

// History navigation (back/forward)
const navigate = useNavigate();
Expand Down Expand Up @@ -690,9 +704,17 @@ function AppInner() {
const provider = getFastModeProvider(model, {
providersConfig,
resolvedRouteProvider: getRouteForModel(normalizeToCanonical(model)),
codexOauthAccountId,
});
return provider != null && providersConfig[provider]?.serviceTier === "priority";
}, [creationScopeId, getModelForWorkspace, getRouteForModel, providersConfig, selectedWorkspace]);
}, [
codexOauthAccountId,
creationScopeId,
getModelForWorkspace,
getRouteForModel,
providersConfig,
selectedWorkspace,
]);

const fastModeToggleInFlightRef = useRef(false);
const toggleFastMode = useCallback(async () => {
Expand All @@ -707,6 +729,7 @@ function AppInner() {
const provider = getFastModeProvider(model, {
providersConfig,
resolvedRouteProvider: getRouteForModel(normalizeToCanonical(model)),
codexOauthAccountId,
});
if (provider == null) {
fastModeToggleInFlightRef.current = false;
Expand Down Expand Up @@ -736,6 +759,7 @@ function AppInner() {
}
}, [
api,
codexOauthAccountId,
creationScopeId,
getModelForWorkspace,
getRouteForModel,
Expand Down Expand Up @@ -984,6 +1008,7 @@ function AppInner() {
workspaceMetadata,
selectedWorkspace,
creationScopeId,
codexOauthAccountId,
themePreference,
getThinkingLevel: getThinkingLevelForWorkspace,
onSetThinkingLevel: setThinkingLevelFromPalette,
Expand Down
20 changes: 18 additions & 2 deletions src/browser/components/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
useMemo,
} from "react";
import { Lightbulb } from "lucide-react";
import { getCodexOauthProjectPath } from "@/common/utils/providers/codexOauthRouting";
import { MessageListProvider } from "@/browser/features/Messages/MessageListContext";
import { cn } from "@/common/lib/utils";
import { ChatInstructionsChatDecoration } from "@/browser/components/InstructionsTab/AdditionalSystemContextScratchpad";
Expand Down Expand Up @@ -99,6 +100,7 @@ import { useReviews } from "@/browser/hooks/useReviews";
import { ReviewsBanner } from "../ReviewsBanner/ReviewsBanner";
import type { ReviewNoteData } from "@/common/types/review";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
import { useProjectContext } from "@/browser/contexts/ProjectContext";
import {
useBackgroundBashActions,
useBackgroundBashError,
Expand Down Expand Up @@ -467,6 +469,11 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
loadingOlderHistory,
activeBashMonitorCount,
} = workspaceState;
const { getProjectConfig } = useProjectContext();
const accountProjectPath = getCodexOauthProjectPath(meta ?? { projectPath });
const codexOauthAccountId = accountProjectPath
? getProjectConfig(accountProjectPath)?.codexOauthAccountId
: undefined;
const shouldShowPinnedTodoList = workspaceState.todos.length > 0;
const shouldShowReviewsBanner = reviews.reviews.length > 0;
const shouldRenderLoadOlderMessagesButton = hasOlderHistory && !isPixelSnapshotEnvironment();
Expand All @@ -486,6 +493,7 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
api: api ?? undefined,
pendingSendOptions,
providersConfig,
codexOauthAccountId,
});

// Apply message transformations:
Expand Down Expand Up @@ -577,9 +585,17 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
use1M,
autoCompactionThreshold / 100,
undefined,
providersConfig
providersConfig,
{ codexOauthAccountId }
),
[workspaceUsage, pendingModel, use1M, providersConfig, autoCompactionThreshold]
[
workspaceUsage,
pendingModel,
use1M,
providersConfig,
autoCompactionThreshold,
codexOauthAccountId,
]
);

// Show warning when: shouldShowWarning flag is true AND not currently compacting.
Expand Down
2 changes: 2 additions & 0 deletions src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,8 @@ function installProjectSidebarTestDoubles() {
providersExpandedProvider: null,
setProvidersExpandedProvider: () => undefined,
providersStartCoderLogin: false,
codexAccountAction: null,
setCodexAccountAction: () => undefined,
setProvidersStartCoderLogin: () => undefined,
runtimesProjectPath: null,
setRuntimesProjectPath: () => undefined,
Expand Down
5 changes: 5 additions & 0 deletions src/browser/components/ThinkingSelector/ThinkingSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface ThinkingInheritOption {

interface ThinkingSelectorControlProps {
modelString: string | undefined;
codexOauthAccountId?: string;
/** Delegated preferences may inherit a model that is only known at launch. */
modelCapabilitiesDeferred?: boolean;
/** Independent of effort/model inheritance; false denotes an explicit mode override. */
Expand Down Expand Up @@ -130,12 +131,14 @@ export const ThinkingSelectorControl: React.FC<ThinkingSelectorControlProps> = (
openaiProModeAvailable(props.modelString, {
providersConfig,
resolvedRouteProvider: resolvedRoute,
codexOauthAccountId: props.codexOauthAccountId,
}),
fastModeProvider:
props.allowFastMode !== false && providersConfig != null
? getFastModeProvider(props.modelString, {
providersConfig,
resolvedRouteProvider: resolvedRoute,
codexOauthAccountId: props.codexOauthAccountId,
})
: null,
};
Expand Down Expand Up @@ -437,6 +440,7 @@ export const ThinkingSelectorControl: React.FC<ThinkingSelectorControlProps> = (

interface ThinkingSelectorProps {
modelString: string;
codexOauthAccountId?: string;
/** Some embedded clients cannot resolve route-aware provider options safely. */
allowProMode?: boolean;
/** Some embedded clients do not expose provider configuration mutations. */
Expand All @@ -451,6 +455,7 @@ export const ThinkingSelector: React.FC<ThinkingSelectorProps> = (props) => {
return (
<ThinkingSelectorControl
modelString={props.modelString}
codexOauthAccountId={props.codexOauthAccountId}
thinkingLevel={thinkingLevel}
onThinkingLevelChange={setThinkingLevel}
reasoningMode={reasoningMode}
Expand Down
26 changes: 26 additions & 0 deletions src/browser/contexts/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ import {
useRef,
useState,
type ReactNode,
type Dispatch,
type SetStateAction,
} from "react";
import { useRouter } from "@/browser/contexts/RouterContext";

export type CodexAccountSettingsIntent =
| { type: "add" | "default" }
| { type: "reconnect" | "rename" | "disconnect"; accountId: string }
| { type: "project"; projectPath: string };

export interface OpenSettingsOptions {
/** When opening the Providers settings, expand the given provider. */
expandProvider?: string;
/** When opening the Providers settings, start the Coder OAuth login. */
startCoderLogin?: boolean;
/** Open a Codex account operation through the existing settings controls. */
codexAccountAction?: CodexAccountSettingsIntent;
/** When opening the Runtimes settings, pre-select this project scope. */
runtimesProjectPath?: string;
/** When opening the Secrets settings, pre-select this project scope. */
Expand All @@ -41,6 +50,9 @@ interface SettingsContextValue {
providersStartCoderLogin: boolean;
setProvidersStartCoderLogin: (start: boolean) => void;

codexAccountAction: CodexAccountSettingsIntent | null;
setCodexAccountAction: Dispatch<SetStateAction<CodexAccountSettingsIntent | null>>;

/** One-shot hint for RuntimesSection to pre-select a project scope. */
runtimesProjectPath: string | null;
setRuntimesProjectPath: (path: string | null) => void;
Expand Down Expand Up @@ -68,6 +80,9 @@ export function SettingsProvider(props: { children: ReactNode }) {
const router = useRouter();
const [providersExpandedProvider, setProvidersExpandedProvider] = useState<string | null>(null);
const [providersStartCoderLogin, setProvidersStartCoderLogin] = useState(false);
const [codexAccountAction, setCodexAccountAction] = useState<CodexAccountSettingsIntent | null>(
null
);
const [runtimesProjectPath, setRuntimesProjectPath] = useState<string | null>(null);
const [secretsProjectPath, setSecretsProjectPath] = useState<string | null>(null);
const [instructionsProjectPath, setInstructionsProjectPath] = useState<string | null>(null);
Expand All @@ -83,9 +98,14 @@ export function SettingsProvider(props: { children: ReactNode }) {
if (nextSection === "providers") {
setProvidersExpandedProvider(options?.expandProvider ?? null);
setProvidersStartCoderLogin(options?.startCoderLogin ?? false);
// A fresh identity lets repeated commands reach an already-open settings section.
setCodexAccountAction(
options?.codexAccountAction ? { ...options.codexAccountAction } : null
);
} else {
setProvidersExpandedProvider(null);
setProvidersStartCoderLogin(false);
setCodexAccountAction(null);
}
if (nextSection === "runtimes") {
setRuntimesProjectPath(options?.runtimesProjectPath ?? null);
Expand Down Expand Up @@ -121,6 +141,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
if (wasOpenRef.current && !isOpen) {
setProvidersExpandedProvider(null);
setProvidersStartCoderLogin(false);
setCodexAccountAction(null);
setRuntimesProjectPath(null);
setSecretsProjectPath(null);
setInstructionsProjectPath(null);
Expand All @@ -134,6 +155,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
const close = useCallback(() => {
setProvidersExpandedProvider(null);
setProvidersStartCoderLogin(false);
setCodexAccountAction(null);
setRuntimesProjectPath(null);
setSecretsProjectPath(null);
setInstructionsProjectPath(null);
Expand All @@ -145,6 +167,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
if (section !== "providers") {
setProvidersExpandedProvider(null);
setProvidersStartCoderLogin(false);
setCodexAccountAction(null);
}
if (section !== "runtimes") {
// Runtime scope hints are one-shot and should not persist across section changes.
Expand Down Expand Up @@ -173,6 +196,8 @@ export function SettingsProvider(props: { children: ReactNode }) {
setProvidersExpandedProvider,
providersStartCoderLogin,
setProvidersStartCoderLogin,
codexAccountAction,
setCodexAccountAction,
runtimesProjectPath,
setRuntimesProjectPath,
secretsProjectPath,
Expand All @@ -189,6 +214,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
registerOnClose,
providersExpandedProvider,
providersStartCoderLogin,
codexAccountAction,
runtimesProjectPath,
secretsProjectPath,
instructionsProjectPath,
Expand Down
53 changes: 44 additions & 9 deletions src/browser/features/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ import type {
import { CreationControls } from "./CreationControls";
import { SEND_DISPATCH_MODES } from "./sendDispatchModes";
import { CodexOauthWarningBanner } from "./CodexOauthWarningBanner";
import {
getCodexOauthProjectPath,
hasCodexOauthTokens,
} from "@/common/utils/providers/codexOauthRouting";
import { useCreationWorkspace } from "./useCreationWorkspace";
import { useCoderWorkspace } from "@/browser/hooks/useCoderWorkspace";
import { useTutorial } from "@/browser/contexts/TutorialContext";
Expand Down Expand Up @@ -234,7 +238,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
[effectivePolicy]
);
const { variant } = props;
const { userProjects } = useProjectContext();
const { userProjects, getProjectConfig } = useProjectContext();
const creationScope =
variant === "creation"
? resolveWorkspaceCreationScope(props.projectPath, userProjects, props.pendingSubProjectPath)
Expand Down Expand Up @@ -491,7 +495,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
);

const { open } = useSettings();
const { selectedWorkspace, beginWorkspaceCreation } = useWorkspaceContext();
const { selectedWorkspace, workspaceMetadata, beginWorkspaceCreation } = useWorkspaceContext();
const { agentId, currentAgent, agents } = useAgent();

// Use current agent's uiColor, or neutral border until agents load
Expand All @@ -505,7 +509,6 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
ensureModelInSettings,
defaultModel,
setDefaultModel,
codexOauthSet,
requiresCodexOauth,
} = useModelsFromSettings();

Expand Down Expand Up @@ -601,17 +604,46 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
const usage = useWorkspaceUsage(workspaceIdForUsage);
const { has1MContext } = useProviderOptions();
const { config: providersConfig } = useProvidersConfig();
const accountProjectPath = getCodexOauthProjectPath(
variant === "creation"
? { projectPath: creationParentProjectPath, subProjectPath: creationSubProjectPath }
: (workspaceMetadata.get(props.workspaceId) ??
(selectedWorkspace?.workspaceId === props.workspaceId ? selectedWorkspace : undefined))
);
const codexOauthAccountId = accountProjectPath
? getProjectConfig(accountProjectPath)?.codexOauthAccountId
: undefined;
Comment thread
coadler marked this conversation as resolved.
const codexOauthSet =
providersConfig == null
? null
: hasCodexOauthTokens(providersConfig.openai, codexOauthAccountId);
const lastUsage = usage?.liveUsage ?? usage?.lastContextUsage;
// Token counts come from usage metadata, but context limits/1M eligibility should
// follow the currently selected model unless a stream is actively running.
const activeUsageModel = usage?.liveUsage?.model ?? null;
const activeUsageModel = usage?.liveModel ?? usage?.liveUsage?.model ?? null;
const contextDisplayModel = activeUsageModel ?? baseModel;
const use1M = has1MContext(contextDisplayModel);
const liveContextLimit = usage?.liveContextLimit;
const contextUsageData = useMemo(() => {
return lastUsage
? calculateTokenMeterData(lastUsage, contextDisplayModel, use1M, false, providersConfig)
: { segments: [], totalTokens: 0, totalPercentage: 0 };
}, [lastUsage, contextDisplayModel, use1M, providersConfig]);
return calculateTokenMeterData(
lastUsage,
contextDisplayModel,
use1M,
false,
providersConfig,
{
codexOauthAccountId,
},
liveContextLimit
);
}, [
lastUsage,
contextDisplayModel,
use1M,
providersConfig,
codexOauthAccountId,
liveContextLimit,
]);
const { threshold: autoCompactThreshold, setThreshold: setAutoCompactThreshold } =
useAutoCompactionSettings(workspaceIdForUsage, contextDisplayModel);
const autoCompactionProps = useMemo(
Expand Down Expand Up @@ -2765,7 +2797,10 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
className="flex shrink-0 items-center"
data-component="ThinkingSelectorGroup"
>
<ThinkingSelector modelString={baseModel} />
<ThinkingSelector
modelString={baseModel}
codexOauthAccountId={codexOauthAccountId}
/>
</div>
</div>

Expand Down
1 change: 1 addition & 0 deletions src/browser/features/ChatInput/useCreationWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export function useCreationWorkspace({
debounceMs: 500,
userModel,
scopeId: workspaceNameScopeId,
projectPath: (subProjectPath ?? projectPath) || undefined,
});

// Destructure name state functions for use in callbacks
Expand Down
Loading
Loading