From 5730ddc7af71c6713b3574a27c99c46b6509f2ad Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 7 May 2026 21:18:39 +0300 Subject: [PATCH] fix(team): harden codex login and runtime previews --- README.md | 13 + docs/team-management/README.md | 1 + docs/team-management/debugging-agent-teams.md | 28 +++ .../hooks/useGraphMemberLogPreviews.ts | 2 +- .../renderer/ui/GraphMemberLogPreviewHud.tsx | 34 ++- src/features/codex-account/contracts/dto.ts | 1 + .../composition/createCodexAccountFeature.ts | 1 + .../CodexLoginSessionManager.ts | 22 +- .../infrastructure/codexAppServer/protocol.ts | 9 + .../services/team/TeamProvisioningService.ts | 146 +++++++---- src/main/services/team/runtimeTeammateMode.ts | 42 ++-- .../components/dashboard/CliStatusBanner.tsx | 12 +- .../components/dashboard/TmuxStatusBanner.tsx | 8 +- .../runtime/CodexLoginLinkCopyButton.tsx | 41 +++- .../runtime/ProviderRuntimeSettingsDialog.tsx | 10 +- .../team/dialogs/CodexReconnectPrompt.tsx | 17 +- .../team/dialogs/CreateTeamDialog.tsx | 1 + .../team/dialogs/LaunchTeamDialog.tsx | 1 + .../main/CodexLoginSessionManager.test.ts | 10 +- .../team/TeamProvisioningService.test.ts | 128 ++++++++++ .../services/team/runtimeTeammateMode.test.ts | 61 +++++ .../cli/CliStatusVisibility.test.ts | 2 +- .../ProviderRuntimeSettingsDialog.test.ts | 2 +- .../GraphMemberLogPreviewHud.test.tsx | 231 ++++++++++++++++++ .../useGraphMemberLogPreviews.test.tsx | 15 +- 25 files changed, 735 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index d771585a..f3e1989f 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,19 @@ pnpm dev The app auto-discovers Claude Code projects from `~/.claude/`. +### Debug teammate runtimes + +Development launches use the app-managed process backend for teammates by default. To inspect +teammates in `tmux` panes while debugging, start the desktop app with: + +```bash +CLAUDE_TEAM_TEAMMATE_MODE=tmux pnpm dev +``` + +The same override is available per launch from custom CLI args with +`--teammate-mode tmux`. Use this as an operator/debug mode; the default process backend provides +stronger app-owned lifecycle, diagnostics, and cleanup for normal team launches. + ### Build for distribution ```bash diff --git a/docs/team-management/README.md b/docs/team-management/README.md index 587c378b..3a8d06fb 100644 --- a/docs/team-management/README.md +++ b/docs/team-management/README.md @@ -22,6 +22,7 @@ | [openclaw-agent-teams-integration.md](./openclaw-agent-teams-integration.md) | How to connect OpenClaw or another outside AI through Agent Teams MCP and REST control API | | [research-worktrees.md](./research-worktrees.md) | Git worktrees + teams, запуск Claude процессов из UI (Phase 2) | | [task-queue-derived-agenda-plan.md](./task-queue-derived-agenda-plan.md) | Подробный rollout-plan по разделению queue/inventory, derived actionOwner и phased agenda/delta sync | +| [debugging-agent-teams.md](./debugging-agent-teams.md) | Runtime debugging runbook, включая `CLAUDE_TEAM_TEAMMATE_MODE=tmux` для pane-backed teammate debug | ## Ключевые решения diff --git a/docs/team-management/debugging-agent-teams.md b/docs/team-management/debugging-agent-teams.md index a6baac42..cf48676f 100644 --- a/docs/team-management/debugging-agent-teams.md +++ b/docs/team-management/debugging-agent-teams.md @@ -52,6 +52,34 @@ Primary launch and OpenCode secondary lanes are different paths. When a launch hangs at `Prepared communication channels for X/Y members`, check whether `Y` incorrectly includes secondary OpenCode members. The filesystem monitor should wait for `effectiveMembers`, not every requested member. +## Teammate Runtime Debug Mode + +Desktop launches use the app-managed process backend by default. That is the supported default for +normal app launches because the app owns the process lifecycle, runtime logs, cleanup, and bootstrap +evidence. + +For local debugging, force pane-backed teammates through `tmux`: + +```bash +CLAUDE_TEAM_TEAMMATE_MODE=tmux pnpm dev +``` + +For a single launch from the UI, add this to custom CLI args: + +```bash +--teammate-mode tmux +``` + +Expected behavior: +- `tmux` mode should remove `CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES` from the launch env. +- The desktop app should pass `--teammate-mode tmux` to the runtime CLI. +- The orchestrator should report `backend_type: "tmux"` and `tmux_pane_id` like `%1`. +- If `tmux` is unavailable, the launch dialog should block explicit tmux mode with a tmux readiness message. + +Use this mode to inspect interactive CLI behavior, terminal prompts, and pane output. Do not treat it +as equivalent to the process backend for recovery semantics; persisted pane IDs can help discovery, +but app restart does not make old panes a fully app-owned runtime again. + ## Member State Meanings Common `launch-state.json` cases: diff --git a/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts b/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts index 8630f328..65be098e 100644 --- a/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts +++ b/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts @@ -382,7 +382,7 @@ export function useGraphMemberLogPreviews(input: { return; } if (event.type === 'task-log-change') { - scheduleReload(false); + scheduleReload(true); } }); diff --git a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx index a0bd4002..76fd117b 100644 --- a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx @@ -62,6 +62,10 @@ function normalizeMemberName(value: string): string { return value.trim().toLowerCase(); } +function buildRenderedItemKey(memberName: string, itemId: string): string { + return `${normalizeMemberName(memberName)}:${itemId}`; +} + function formatRelativeTime(timestamp: string): string { const parsed = Date.parse(timestamp); if (!Number.isFinite(parsed)) return ''; @@ -145,7 +149,11 @@ function trimRepeatedTitlePrefix(preview: string, title: string): string { function compactPreviewText(item: MemberLogPreviewItem, displayTitle: string): string { const preview = item.preview?.trim(); if (preview) { - const compact = trimRepeatedTitlePrefix(preview, displayTitle); + const rawTitle = item.title.trim(); + const compact = trimRepeatedTitlePrefix( + trimRepeatedTitlePrefix(preview, rawTitle), + displayTitle + ); return compact || preview; } if (item.kind === 'tool_result') { @@ -244,45 +252,45 @@ export const GraphMemberLogPreviewHud = ({ useEffect(() => { if (!enabled) return; - const newItemIds: string[] = []; + const newItemKeys: string[] = []; for (const [memberKey, preview] of previewsByMember) { const currentIds = new Set(preview.items.map((item) => item.id)); const knownIds = knownItemIdsByMemberRef.current.get(memberKey); if (knownIds) { for (const itemId of currentIds) { if (!knownIds.has(itemId)) { - newItemIds.push(itemId); + newItemKeys.push(buildRenderedItemKey(memberKey, itemId)); } } } knownItemIdsByMemberRef.current.set(memberKey, currentIds); } - if (newItemIds.length === 0) return; + if (newItemKeys.length === 0) return; setHighlightedItemIds((current) => { const next = new Set(current); - for (const itemId of newItemIds) { - next.add(itemId); + for (const itemKey of newItemKeys) { + next.add(itemKey); } return next; }); - for (const itemId of newItemIds) { - const existingTimer = highlightTimersRef.current.get(itemId); + for (const itemKey of newItemKeys) { + const existingTimer = highlightTimersRef.current.get(itemKey); if (existingTimer) { clearTimeout(existingTimer); } const timer = setTimeout(() => { - highlightTimersRef.current.delete(itemId); + highlightTimersRef.current.delete(itemKey); setHighlightedItemIds((current) => { - if (!current.has(itemId)) return current; + if (!current.has(itemKey)) return current; const next = new Set(current); - next.delete(itemId); + next.delete(itemKey); return next; }); }, NEW_LOG_HIGHLIGHT_MS); - highlightTimersRef.current.set(itemId, timer); + highlightTimersRef.current.set(itemKey, timer); } }, [enabled, previewsByMember]); @@ -424,7 +432,7 @@ export const GraphMemberLogPreviewHud = ({ const titleText = relativeTime ? `${displayTitle} ${relativeTime} ${fullPreviewText}` : `${displayTitle} ${fullPreviewText}`; - const isHighlighted = highlightedItemIds.has(item.id); + const isHighlighted = highlightedItemIds.has(buildRenderedItemKey(memberName, item.id)); const isError = item.tone === 'error'; const rowStateClassName = isHighlighted ? isError diff --git a/src/features/codex-account/contracts/dto.ts b/src/features/codex-account/contracts/dto.ts index 7e8b8361..abbb17be 100644 --- a/src/features/codex-account/contracts/dto.ts +++ b/src/features/codex-account/contracts/dto.ts @@ -63,6 +63,7 @@ export interface CodexLoginStateDto { error: string | null; startedAt: string | null; authUrl?: string | null; + userCode?: string | null; } export interface CodexRuntimeContextDto { diff --git a/src/features/codex-account/main/composition/createCodexAccountFeature.ts b/src/features/codex-account/main/composition/createCodexAccountFeature.ts index 05cb12a3..a7aa9d63 100644 --- a/src/features/codex-account/main/composition/createCodexAccountFeature.ts +++ b/src/features/codex-account/main/composition/createCodexAccountFeature.ts @@ -693,6 +693,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade { error: loginState.status === 'failed' ? loginState.error : null, startedAt: null, authUrl: null, + userCode: null, }; } diff --git a/src/features/codex-account/main/infrastructure/CodexLoginSessionManager.ts b/src/features/codex-account/main/infrastructure/CodexLoginSessionManager.ts index a5284c6a..fc86ca10 100644 --- a/src/features/codex-account/main/infrastructure/CodexLoginSessionManager.ts +++ b/src/features/codex-account/main/infrastructure/CodexLoginSessionManager.ts @@ -26,6 +26,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }; private pendingStartToken: symbol | null = null; private activeSession: { @@ -72,6 +73,7 @@ export class CodexLoginSessionManager { error: null, startedAt: new Date().toISOString(), authUrl: null, + userCode: null, }); try { @@ -89,7 +91,7 @@ export class CodexLoginSessionManager { const response = await session.request( 'account/login/start', - { type: 'chatgpt' }, + { type: 'chatgptDeviceCode' }, LOGIN_REQUEST_TIMEOUT_MS ); @@ -98,15 +100,19 @@ export class CodexLoginSessionManager { return; } - if (response.type !== 'chatgpt') { + if (response.type !== 'chatgptDeviceCode') { throw new Error('Codex app-server returned an unexpected login response type'); } - const authUrl = new URL(response.authUrl); + const authUrl = new URL(response.verificationUrl); if (authUrl.protocol !== 'https:') { throw new Error('Codex app-server returned a non-https auth URL'); } + if (!response.userCode.trim()) { + throw new Error('Codex app-server returned an empty ChatGPT login code'); + } + const disposeNotificationListener = session.onNotification((method, params) => { if (method !== 'account/login/completed') { return; @@ -137,6 +143,7 @@ export class CodexLoginSessionManager { error: null, startedAt: this.state.startedAt, authUrl: authUrl.toString(), + userCode: response.userCode, }); } catch (error) { const wasAbandonedDuringStart = @@ -159,6 +166,7 @@ export class CodexLoginSessionManager { error: error instanceof Error ? error.message : String(error), startedAt: this.state.startedAt, authUrl: this.state.authUrl, + userCode: this.state.userCode, }); throw error; } @@ -172,6 +180,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }); this.emitSettled(); return; @@ -183,6 +192,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }); return; } @@ -211,6 +221,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }); this.emitSettled(); } @@ -226,6 +237,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }); return; } @@ -240,6 +252,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }); } @@ -262,6 +275,7 @@ export class CodexLoginSessionManager { error: null, startedAt: null, authUrl: null, + userCode: null, }); } else { this.setState({ @@ -269,6 +283,7 @@ export class CodexLoginSessionManager { error: notification.error ?? 'ChatGPT login failed.', startedAt: this.state.startedAt, authUrl: this.state.authUrl, + userCode: this.state.userCode, }); } @@ -290,6 +305,7 @@ export class CodexLoginSessionManager { error: errorMessage, startedAt: this.state.startedAt, authUrl: this.state.authUrl, + userCode: this.state.userCode, }); this.emitSettled(); } diff --git a/src/main/services/infrastructure/codexAppServer/protocol.ts b/src/main/services/infrastructure/codexAppServer/protocol.ts index e6bb3739..eb41f37e 100644 --- a/src/main/services/infrastructure/codexAppServer/protocol.ts +++ b/src/main/services/infrastructure/codexAppServer/protocol.ts @@ -43,6 +43,9 @@ export type CodexAppServerLoginAccountParams = | { type: 'chatgpt'; } + | { + type: 'chatgptDeviceCode'; + } | { type: 'chatgptAuthTokens'; accessToken: string; @@ -57,6 +60,12 @@ export type CodexAppServerLoginAccountResponse = loginId: string; authUrl: string; } + | { + type: 'chatgptDeviceCode'; + loginId: string; + verificationUrl: string; + userCode: string; + } | { type: 'chatgptAuthTokens' }; export type CodexAppServerLogoutAccountResponse = Record; diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 34b76553..b204c5bd 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -16496,54 +16496,10 @@ export class TeamProvisioningService { stdoutLineBuf += text; const lines = stdoutLineBuf.split('\n'); stdoutLineBuf = lines.pop() ?? ''; - run.stdoutParserCarry = stdoutLineBuf; - const trimmedCarry = stdoutLineBuf.trim(); - if (!trimmedCarry) { - run.stdoutParserCarryIsCompleteJson = false; - run.stdoutParserCarryLooksLikeClaudeJson = false; - } else { - try { - JSON.parse(trimmedCarry); - run.stdoutParserCarryIsCompleteJson = true; - } catch { - run.stdoutParserCarryIsCompleteJson = false; - } - run.stdoutParserCarryLooksLikeClaudeJson = looksLikeClaudeStdoutJsonFragment(trimmedCarry); - } + this.updateStdoutParserCarry(run, stdoutLineBuf); for (const line of lines) { const trimmed = line.trim(); - if (!trimmed) continue; - try { - const msg = JSON.parse(trimmed) as Record; - // Only reset stall timer on messages that represent actual API progress - // (assistant response or result). System messages like retry attempts - // (type=system, subtype=attempt) are informational — the CLI is still - // waiting for the API and the user should see the stall warning. - const msgType = msg.type; - if (msgType === 'assistant' || msgType === 'result') { - run.lastStdoutReceivedAt = Date.now(); - if (run.stallWarningIndex != null) { - const removedIndex = run.stallWarningIndex; - run.provisioningOutputParts.splice(removedIndex, 1); - this.shiftProvisioningOutputIndexesAfterRemoval(run, removedIndex); - run.stallWarningIndex = null; - if (run.preStallMessage != null) { - run.progress.message = run.preStallMessage; - run.preStallMessage = null; - delete run.progress.messageSeverity; - } - } - } - this.handleStreamJsonMessage(run, msg); - } catch { - // Not valid JSON — check for auth failure in raw text output - this.handleAuthFailureInOutput(run, trimmed, 'stdout'); - if (this.hasApiError(trimmed) && !this.isAuthFailureWarning(trimmed, 'stdout')) { - // Show warning but do NOT kill — the SDK may be retrying internally (e.g. 429 model_cooldown). - // If all retries fail, result.subtype="error" will catch it and kill then. - this.emitApiErrorWarning(run, trimmed); - } - } + this.handleStdoutParserLine(run, trimmed); } const currentTs = Date.now(); @@ -16554,6 +16510,76 @@ export class TeamProvisioningService { }); } + private updateStdoutParserCarry(run: ProvisioningRun, carry: string): void { + run.stdoutParserCarry = carry; + const trimmedCarry = carry.trim(); + if (!trimmedCarry) { + run.stdoutParserCarryIsCompleteJson = false; + run.stdoutParserCarryLooksLikeClaudeJson = false; + return; + } + + try { + JSON.parse(trimmedCarry); + run.stdoutParserCarryIsCompleteJson = true; + } catch { + run.stdoutParserCarryIsCompleteJson = false; + } + run.stdoutParserCarryLooksLikeClaudeJson = looksLikeClaudeStdoutJsonFragment(trimmedCarry); + } + + private flushStdoutParserCarry(run: ProvisioningRun): void { + const trimmed = run.stdoutParserCarry.trim(); + if (!trimmed || !run.stdoutParserCarryIsCompleteJson) { + return; + } + + this.handleStdoutParserLine(run, trimmed); + this.updateStdoutParserCarry(run, ''); + } + + private handleStdoutParserLine(run: ProvisioningRun, trimmed: string): void { + if (!trimmed) { + return; + } + + try { + const msg = JSON.parse(trimmed) as Record; + this.handleParsedStdoutJsonMessage(run, msg); + } catch { + // Not valid JSON - check for auth failure in raw text output. + this.handleAuthFailureInOutput(run, trimmed, 'stdout'); + if (this.hasApiError(trimmed) && !this.isAuthFailureWarning(trimmed, 'stdout')) { + // Show warning but do not kill - the SDK may be retrying internally (e.g. 429 model_cooldown). + // If all retries fail, result.subtype="error" will catch it and kill then. + this.emitApiErrorWarning(run, trimmed); + } + } + } + + private handleParsedStdoutJsonMessage(run: ProvisioningRun, msg: Record): void { + // Only reset stall timer on messages that represent actual API progress + // (assistant response or result). System messages like retry attempts + // (type=system, subtype=attempt) are informational - the CLI is still + // waiting for the API and the user should see the stall warning. + const msgType = msg.type; + if (msgType === 'assistant' || msgType === 'result') { + run.lastStdoutReceivedAt = Date.now(); + if (run.stallWarningIndex != null) { + const removedIndex = run.stallWarningIndex; + run.provisioningOutputParts.splice(removedIndex, 1); + this.shiftProvisioningOutputIndexesAfterRemoval(run, removedIndex); + run.stallWarningIndex = null; + if (run.preStallMessage != null) { + run.progress.message = run.preStallMessage; + run.preStallMessage = null; + delete run.progress.messageSeverity; + } + } + } + this.handleStreamJsonMessage(run, msg); + } + /** Attaches the stderr handler with auth failure detection. */ private attachStderrHandler(run: ProvisioningRun): void { const child = run.child; @@ -20364,7 +20390,7 @@ export class TeamProvisioningService { private markUnconfirmedBootstrapMembersFailed( run: ProvisioningRun, reason: string, - options?: { cleanupRequested?: boolean } + options?: { cleanupRequested?: boolean; preserveExistingFailure?: boolean } ): void { const failedAt = nowIso(); const baseReason = reason.trim() || 'Deterministic bootstrap failed before teammate check-in.'; @@ -20373,6 +20399,15 @@ export class TeamProvisioningService { if (prev.bootstrapConfirmed || prev.skippedForLaunch) { continue; } + const hasExistingFailure = + prev.status === 'error' || + prev.launchState === 'failed_to_start' || + prev.hardFailure === true || + Boolean(prev.error) || + Boolean(prev.hardFailureReason); + if (options?.preserveExistingFailure && hasExistingFailure) { + continue; + } const runtimeWasAlive = prev.runtimeAlive === true || prev.livenessSource === 'process'; const hardFailureReason = runtimeWasAlive @@ -28315,11 +28350,16 @@ export class TeamProvisioningService { } if (!hasNewerTrackedRun && run.isLaunch && !run.provisioningComplete && !run.cancelRequested) { - this.markUnconfirmedBootstrapMembersFailed( - run, - 'Launch ended before teammate bootstrap completed.', - { cleanupRequested: true } - ); + const cleanupReason = + typeof run.progress.error === 'string' && run.progress.error.trim() + ? run.progress.error.trim() + : run.progress.state === 'failed' && run.progress.message.trim() + ? run.progress.message.trim() + : 'Launch ended before teammate bootstrap completed.'; + this.markUnconfirmedBootstrapMembersFailed(run, cleanupReason, { + cleanupRequested: true, + preserveExistingFailure: true, + }); void this.persistLaunchStateSnapshot(run, 'finished'); } this.resetRuntimeToolActivity(run); @@ -28615,6 +28655,8 @@ export class TeamProvisioningService { return; } + this.flushStdoutParserCarry(run); + // IMPORTANT: stopStallWatchdog MUST be AFTER authRetryInProgress guard above! // During respawn, the old process exit fires but run.stallCheckHandle already // points to the NEW process's watchdog. Stopping it here would kill the wrong timer. diff --git a/src/main/services/team/runtimeTeammateMode.ts b/src/main/services/team/runtimeTeammateMode.ts index a61af14e..84c0b183 100644 --- a/src/main/services/team/runtimeTeammateMode.ts +++ b/src/main/services/team/runtimeTeammateMode.ts @@ -6,11 +6,13 @@ interface DesktopTeammateModeDecision { forceProcessTeammates: boolean; } +type DesktopTeammateMode = 'auto' | 'tmux' | 'in-process'; + +const DESKTOP_TEAMMATE_MODE_ENV = 'CLAUDE_TEAM_TEAMMATE_MODE'; + let tmuxAvailablePromise: Promise | null = null; -function getExplicitTeammateMode( - rawExtraCliArgs: string | undefined -): 'auto' | 'tmux' | 'in-process' | null { +function getExplicitTeammateMode(rawExtraCliArgs: string | undefined): DesktopTeammateMode | null { const tokens = parseCliArgs(rawExtraCliArgs); for (let i = 0; i < tokens.length; i += 1) { const token = tokens[i]; @@ -34,6 +36,17 @@ function getExplicitTeammateMode( return null; } +function normalizeDesktopTeammateMode(value: string | undefined): DesktopTeammateMode | null { + const normalized = value?.trim().toLowerCase(); + return normalized === 'auto' || normalized === 'tmux' || normalized === 'in-process' + ? normalized + : null; +} + +function getEnvTeammateMode(env: NodeJS.ProcessEnv): DesktopTeammateMode | null { + return normalizeDesktopTeammateMode(env[DESKTOP_TEAMMATE_MODE_ENV]); +} + async function isTmuxAvailable(): Promise { if (!tmuxAvailablePromise) { tmuxAvailablePromise = isTmuxRuntimeReadyForCurrentPlatform() @@ -48,24 +61,25 @@ async function isTmuxAvailable(): Promise { } export async function resolveDesktopTeammateModeDecision( - rawExtraCliArgs: string | undefined + rawExtraCliArgs: string | undefined, + env: NodeJS.ProcessEnv = process.env ): Promise { - const explicitMode = getExplicitTeammateMode(rawExtraCliArgs); - if (explicitMode === 'tmux') { + const requestedMode = getExplicitTeammateMode(rawExtraCliArgs) ?? getEnvTeammateMode(env); + if (requestedMode === 'tmux') { + return { + injectedTeammateMode: 'tmux', + forceProcessTeammates: false, + }; + } + + if (requestedMode === 'auto') { return { injectedTeammateMode: null, forceProcessTeammates: true, }; } - if (explicitMode === 'auto') { - return { - injectedTeammateMode: null, - forceProcessTeammates: true, - }; - } - - if (explicitMode === 'in-process') { + if (requestedMode === 'in-process') { return { injectedTeammateMode: null, forceProcessTeammates: false, diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index 93e9d036..312f8ec7 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -19,7 +19,10 @@ import { import { api, isElectronMode } from '@renderer/api'; import { confirm } from '@renderer/components/common/ConfirmDialog'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; -import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton'; +import { + CodexLoginLinkCopyButton, + CodexLoginUserCodeBadge, +} from '@renderer/components/runtime/CodexLoginLinkCopyButton'; import { formatProviderStatusText, getProviderConnectionModeSummary, @@ -103,7 +106,9 @@ function getCodexDashboardHint(provider: CliProviderStatus): string | null { } if (codex.login.status === 'starting' || codex.login.status === 'pending') { - return codex.login.authUrl ? 'Finish ChatGPT login in the browser.' : null; + return codex.login.authUrl + ? 'Finish ChatGPT login in the browser. Enter the shown code if prompted.' + : null; } const usageHint = codex.localActiveChatgptAccountPresent @@ -718,6 +723,7 @@ const InstalledBanner = ({ provider.connection?.codex?.login.status !== 'starting' && provider.connection?.codex?.login.status !== 'pending'; const codexLoginAuthUrl = provider.connection?.codex?.login.authUrl ?? null; + const codexLoginUserCode = provider.connection?.codex?.login.userCode ?? null; const showCodexLoginActions = codexNeedsReconnect || Boolean(codexLoginAuthUrl); const disconnectAction = getProviderDisconnectAction(provider); const providerLoading = cliProviderStatusLoading[provider.providerId] === true; @@ -888,9 +894,11 @@ const InstalledBanner = ({ <> + ); }; + +export const CodexLoginUserCodeBadge = ({ + userCode, +}: { + userCode?: string | null; +}): React.JSX.Element | null => { + if (!userCode) { + return null; + } + + return ( + + Code {userCode} + + ); +}; diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx index e8c72d7f..3be99a6e 100644 --- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx @@ -22,7 +22,10 @@ import { import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer'; import { api } from '@renderer/api'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; -import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton'; +import { + CodexLoginLinkCopyButton, + CodexLoginUserCodeBadge, +} from '@renderer/components/runtime/CodexLoginLinkCopyButton'; import { Button } from '@renderer/components/ui/button'; import { Dialog, @@ -718,6 +721,7 @@ export const ProviderRuntimeSettingsDialog = ({ const codexLoginPending = codexConnection?.login.status === 'starting' || codexConnection?.login.status === 'pending'; const codexLoginAuthUrl = codexConnection?.login.authUrl ?? null; + const codexLoginUserCode = codexConnection?.login.userCode ?? null; const configurableAuthModes = selectedProvider?.connection?.configurableAuthModes ?? []; const configuredAuthMode: CliProviderAuthMode | undefined = selectedProvider?.connection?.configuredAuthMode ?? configurableAuthModes[0] ?? undefined; @@ -1395,8 +1399,10 @@ export const ProviderRuntimeSettingsDialog = ({ <> + {codexLoginAuthUrl ? (