Skip to content

Commit 5727a09

Browse files
committed
fix: ChatGPT OAuth probe + ModelSwitcher density + streamingLabel i18n
- connection-ipc: ChatGPT subscription wire (openai-codex-responses) has no /models endpoint; a generic Bearer probe always 401s and surfaces as misleading "API key 错误". Check the OAuth token store directly and return a login-specific hint instead. - ModelSwitcher: raise search-input visibility threshold 8→12 (a 12- model list is still scannable); drop the per-provider header chrome in dropdown variant; tighten the search box (transparent bg, no border) so it reads as part of the menu, not a nested form. - i18n: add chat.streamingLabel ("Assistant is typing" / "助手正在回复"). Signed-off-by: hqhq1025 <1506751656@qq.com>
1 parent 4a07c39 commit 5727a09

4 files changed

Lines changed: 54 additions & 18 deletions

File tree

apps/desktop/src/main/connection-ipc.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
stripInferenceEndpointSuffix,
1212
} from '@open-codesign/shared';
1313
import { buildAuthHeaders, buildAuthHeadersForWire } from './auth-headers';
14+
import { getCodexTokenStore } from './codex-oauth-ipc';
1415
import { ipcMain } from './electron-runtime';
1516
import { getApiKeyForProvider, getCachedConfig } from './onboarding-ipc';
1617
import { isKeylessProviderAllowed } from './provider-settings';
@@ -414,7 +415,47 @@ function resolveActiveCredentials(): ActiveProviderCredentials | ConnectionTestE
414415
return resolveCredentialsForProvider(active);
415416
}
416417

418+
async function testChatGPTCodexOAuth(): Promise<ConnectionTestResponse> {
419+
let stored: Awaited<ReturnType<ReturnType<typeof getCodexTokenStore>['read']>>;
420+
try {
421+
stored = await getCodexTokenStore().read();
422+
} catch (err) {
423+
return {
424+
ok: false,
425+
code: '401',
426+
message: err instanceof Error ? err.message : String(err),
427+
hint: 'ChatGPT 订阅凭证读取失败,请到 Settings 重新登录',
428+
};
429+
}
430+
if (stored === null) {
431+
return {
432+
ok: false,
433+
code: '401',
434+
message: 'No ChatGPT OAuth token stored',
435+
hint: 'ChatGPT 订阅未登录,请到 Settings 登录',
436+
};
437+
}
438+
if (stored.expiresAt < Date.now()) {
439+
return {
440+
ok: false,
441+
code: '401',
442+
message: 'ChatGPT OAuth token expired',
443+
hint: 'ChatGPT 订阅登录已过期,请重新登录',
444+
};
445+
}
446+
return { ok: true };
447+
}
448+
417449
async function runProviderTest(creds: ActiveProviderCredentials): Promise<ConnectionTestResponse> {
450+
// ChatGPT subscription uses OAuth + ChatGPT-Account-Id headers; its host
451+
// has no `/models` endpoint that a generic Bearer probe can reach. A plain
452+
// HTTP probe would return 401 here and render as the misleading "API key
453+
// 错误或权限不足" hint — so we check the OAuth token store directly and
454+
// surface a login-specific hint instead.
455+
if (creds.wire === 'openai-codex-responses') {
456+
return testChatGPTCodexOAuth();
457+
}
458+
418459
const { url } = buildEndpointForWire(creds.wire, creds.baseUrl);
419460
const headers = buildAuthHeadersForWire(
420461
creds.wire,

apps/desktop/src/renderer/src/components/ModelSwitcher.tsx

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ interface ModelSwitcherProps {
1010
}
1111

1212
// Below this threshold the search input just adds UI chrome for no real win —
13-
// a user with 6 models can eyeball the list. Above it, scrolling becomes a
14-
// chore (community feedback: providers like DeepSeek/Zhipu return 40+ IDs).
15-
const SEARCH_VISIBILITY_THRESHOLD = 8;
13+
// a user with ~12 models can eyeball and scroll the list without filtering.
14+
// Above it, scrolling becomes a chore (community feedback: providers like
15+
// DeepSeek/Zhipu return 40+ IDs).
16+
const SEARCH_VISIBILITY_THRESHOLD = 12;
1617

1718
function shortenModelLabel(model: string): string {
1819
const stripped = model.replace(/^(claude-|gpt-|gemini-)/, '');
@@ -182,20 +183,8 @@ export function ModelSwitcher({ variant }: ModelSwitcherProps) {
182183
: 'top-full mt-[var(--space-1)] right-0 min-w-[260px]'
183184
}`}
184185
>
185-
{/* Header — show which provider preset these models belong to */}
186-
{!isSidebar && (
187-
<div className="px-[var(--space-3)] py-[var(--space-2)] border-b border-[var(--color-border-muted)]">
188-
<p className="text-[10px] uppercase tracking-[0.1em] text-[var(--color-text-muted)] font-medium">
189-
{t('topbar.modelSwitcher.fromProvider', { defaultValue: 'Provider' })}
190-
</p>
191-
<p className="text-[12px] text-[var(--color-text-primary)] mt-[2px]">
192-
{providerLabel}
193-
</p>
194-
</div>
195-
)}
196-
197186
{showSearch && (
198-
<div className="relative px-[var(--space-2)] py-[var(--space-1_5)] border-b border-[var(--color-border-muted)]">
187+
<div className="relative p-[var(--space-2)] border-b border-[var(--color-border-muted)]">
199188
<Search
200189
className="absolute left-[calc(var(--space-2)+var(--space-2))] top-1/2 -translate-y-1/2 w-[var(--size-icon-xs)] h-[var(--size-icon-xs)] text-[var(--color-text-muted)] pointer-events-none"
201190
aria-hidden
@@ -211,7 +200,7 @@ export function ModelSwitcher({ variant }: ModelSwitcherProps) {
211200
aria-label={t('topbar.modelSwitcher.searchAriaLabel', {
212201
defaultValue: 'Filter models by name',
213202
})}
214-
className="w-full h-[var(--size-control-xs)] pl-[calc(var(--space-2)+var(--size-icon-xs)+var(--space-1_5))] pr-[var(--space-2)] rounded-[var(--radius-sm)] bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--text-xs)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]"
203+
className="w-full h-[var(--size-control-xs)] pl-[calc(var(--space-2)+var(--size-icon-xs)+var(--space-1_5))] pr-[calc(var(--space-2)+var(--size-icon-sm))] rounded-[var(--radius-sm)] bg-transparent border-0 text-[var(--text-xs)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]"
215204
style={{ fontFamily: 'var(--font-mono)' }}
216205
/>
217206
{query.length > 0 && (
@@ -224,7 +213,7 @@ export function ModelSwitcher({ variant }: ModelSwitcherProps) {
224213
aria-label={t('topbar.modelSwitcher.clearSearch', {
225214
defaultValue: 'Clear search',
226215
})}
227-
className="absolute right-[calc(var(--space-2)+var(--space-1_5))] top-1/2 -translate-y-1/2 inline-flex items-center justify-center w-[var(--size-icon-sm)] h-[var(--size-icon-sm)] rounded-full text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors"
216+
className="absolute right-[calc(var(--space-2)+var(--space-1))] top-1/2 -translate-y-1/2 inline-flex items-center justify-center w-[var(--size-icon-sm)] h-[var(--size-icon-sm)] rounded-full text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors"
228217
>
229218
<X className="w-[var(--size-icon-xs)] h-[var(--size-icon-xs)]" aria-hidden />
230219
</button>

packages/i18n/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"trigger": "Add context"
132132
},
133133
"thinking": "Thinking",
134+
"streamingLabel": "Assistant is typing",
134135
"tokensLine": "~{{count}} tokens",
135136
"artifactDelivered": "delivered",
136137
"artifactDefaultLabel": "design.html",
@@ -1017,6 +1018,8 @@
10171018
"PROVIDER_ABORTED": "Generation was cancelled.",
10181019
"PROVIDER_RETRY_EXHAUSTED": "The provider failed after several retries. Check your connection and try again.",
10191020
"CLAUDE_CODE_OAUTH_ONLY": "Your Claude Code login uses an Anthropic subscription (Pro/Max). Third-party apps cannot reuse the subscription quota — generate an API key at console.anthropic.com and use it here.",
1021+
"CODEX_TOKEN_PARSE_FAILED": "Local ChatGPT login is corrupted. Please re-login in Settings.",
1022+
"CODEX_TOKEN_NOT_LOGGED_IN": "ChatGPT subscription is not signed in. Please log in via Settings.",
10201023
"INPUT_EMPTY_PROMPT": "The prompt cannot be empty.",
10211024
"INPUT_EMPTY_COMMENT": "The comment cannot be empty.",
10221025
"INPUT_EMPTY_HTML": "Existing HTML is required for this operation.",

packages/i18n/src/locales/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"trigger": "添加上下文"
132132
},
133133
"thinking": "正在思考",
134+
"streamingLabel": "助手正在回复",
134135
"tokensLine": "约 {{count}} tokens",
135136
"artifactDelivered": "已生成",
136137
"artifactDefaultLabel": "design.html",
@@ -1013,6 +1014,8 @@
10131014
"PROVIDER_ABORTED": "生成已取消。",
10141015
"PROVIDER_RETRY_EXHAUSTED": "多次重试后 provider 仍然失败,请检查网络后重试。",
10151016
"CLAUDE_CODE_OAUTH_ONLY": "你的 Claude Code 使用的是 Anthropic 订阅(Pro/Max),第三方应用无法复用订阅额度——请到 console.anthropic.com 生成 API key 后填入。",
1017+
"CODEX_TOKEN_PARSE_FAILED": "本地 ChatGPT 登录凭证已损坏,请到设置中重新登录。",
1018+
"CODEX_TOKEN_NOT_LOGGED_IN": "ChatGPT 订阅未登录,请到设置中登录。",
10161019
"INPUT_EMPTY_PROMPT": "prompt 不能为空。",
10171020
"INPUT_EMPTY_COMMENT": "评论内容不能为空。",
10181021
"INPUT_EMPTY_HTML": "此操作需要已有的 HTML 内容。",

0 commit comments

Comments
 (0)