diff --git a/Directory.Build.props b/Directory.Build.props index e6087df..413031d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.43.3 + 1.44.0 10.0.13 diff --git a/README.md b/README.md index 907e582..f1aba73 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,20 @@ **A desktop account switcher and quota dashboard for OpenAI Codex.** See your accounts together, choose the next one, and recover an interrupted handoff. Optional quota-based switching starts **Off**. -Windows · macOS15+ · Linux preview · MIT open source · **1.43.3** +Windows · macOS15+ · Linux preview · MIT open source · **1.44.0** [English](README.md) / [한국어](docs/README.ko.md) / [日本語](docs/README.ja.md) / [简体中文](docs/README.zh-CN.md) -[Download](#download) · [Get started](#get-started) · [What's new](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +[Download](#download) · [Get started](#get-started) · [What's new](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) -

Codex Switch UI demo with account quota cards, CPU and memory and GPU usage percentages, and two storage devices

+

Codex Switch1.44.0 with quota tanks and one Settings gear

-Windows UI demo with simulated account and system data. [Demo sources](docs/media/discovery/README.md) / [native verification](docs/evidence/account-automation/README.md). Credentials stay in a local encrypted vault; **switching restarts Codex**. Independent project, not affiliated with OpenAI. +Actual Windows app with fictional account inputs. [Native screenshots and source verification](docs/evidence/home-simplification/README.md). Credentials stay in a local encrypted vault; **switching restarts Codex**. Independent project, not affiliated with OpenAI. **Find it useful?** Star this repository or share it with another Codex user. +Release1.44.0 keeps the quota tanks and direct account switching while gathering models, automatic switching, restart, language and appearance in one Settings gear. Image-heavy chats also gain corrected built-in image routes and bounded concurrent request handling. [What's changed and compatibility](docs/development/RELEASE_1_44_0.md). + Release1.42.2 adds account-specific browser sign-in from warnings, default-Off quota-based account switching, optimal/custom priority, durable reopening/recovery, labeled subscription dates and official Pro 100/200/500 tiers. [Behavior and verification scope](docs/development/ACCOUNT_AUTOMATION.md) / [release gates](docs/development/RELEASE_1_42_2.md). Release1.42.3 recovers Windows usage refresh after Codex replaces its executable, without restarting Switch. Valid paths stay cached; installation gaps keep an explicit warning and requests are never replayed. [Recovery and verification](docs/development/RELEASE_1_42_3.md). @@ -41,25 +43,25 @@ Complete packages include the .NET runtime; no source build or .NET installation | Platform | Download | | --- | --- | -| Windows x64 | [ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-win-x64.zip) | -| macOS15+ Apple Silicon | [ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-arm64.zip) | -| macOS15+ Intel | [ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-x64.zip) | -| Linux x64 **preview** | [DEB](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-preview-linux-x64.deb) / [RPM](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-preview-linux-x64.rpm) / [tar.gz](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-preview-linux-x64.tar.gz) | -| Linux ARM64 **preview** | [DEB](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-preview-linux-arm64.deb) / [RPM](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-preview-linux-arm64.rpm) / [tar.gz](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-preview-linux-arm64.tar.gz) | +| Windows x64 | [ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-win-x64.zip) | +| macOS15+ Apple Silicon | [ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-arm64.zip) | +| macOS15+ Intel | [ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-x64.zip) | +| Linux x64 **preview** | [DEB](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-preview-linux-x64.deb) / [RPM](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-preview-linux-x64.rpm) / [tar.gz](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-preview-linux-x64.tar.gz) | +| Linux ARM64 **preview** | [DEB](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-preview-linux-arm64.deb) / [RPM](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-preview-linux-arm64.rpm) / [tar.gz](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-preview-linux-arm64.tar.gz) | -[Release notes and SHA256 checksums](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3). Windows/Linux packages are unsigned. Mac bundles are ad-hoc signed and **not Apple-notarized**; follow [Mac setup](docs/MACOS.md). +[Release notes and SHA256 checksums](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0). Windows/Linux packages are unsigned. Mac bundles are ad-hoc signed and **not Apple-notarized**; follow [Mac setup](docs/MACOS.md). Linux is an integration preview. Native storage, installed UI and package tests are distinct from supported account handoff and full desktop acceptance. Desktop integration requires approved OpenAI Linux client26.1007.71326, an existing file credential policy and X11/XWayland; newer clients require review. CLI-only mode needs an explicit executable checksum and does not resume desktop chats. [Requirements and limits](docs/development/LINUX.md) / [Linux installation](docs/development/LINUX_PACKAGES.md) Warning icons retain full hover details; activate one to sign in again for the selected account. [Actual application evidence](docs/evidence/account-automation/README.md). -**Linux1.43.3 downloads include terminal account switching and saved CLI-session resumption, introduced in source1.41.0.** `CodexSwitch cli` manages the existing encrypted accounts without DISPLAY; `cli switch --resume ` combines manual switching and official CLI resumption. `--continue` explicitly submits a follow-up; `--exec --continue` selects noninteractive continuation. A verified native CLI and an unlocked supported Secret Service are required. Real two-account contextual CLI resumption is verified on Linux x64; native ARM64/offline tests and full desktop acceptance have separate scopes. [Commands, actual screenshots and exact verification limits](docs/development/LINUX_TERMINAL.md). +**Linux1.44.0 downloads include terminal account switching and saved CLI-session resumption, introduced in source1.41.0.** `CodexSwitch cli` manages the existing encrypted accounts without DISPLAY; `cli switch --resume ` combines manual switching and official CLI resumption. `--continue` explicitly submits a follow-up; `--exec --continue` selects noninteractive continuation. A verified native CLI and an unlocked supported Secret Service are required. Real two-account contextual CLI resumption is verified on Linux x64; native ARM64/offline tests and full desktop acceptance have separate scopes. [Commands, actual screenshots and exact verification limits](docs/development/LINUX_TERMINAL.md). ## Get started 1. Windows: run `CodexSwitch.exe`. Mac: run `Install.command`, then open the installed **Codex Switch.app**. Linux: install a complete preview package and configure a supported Secret Service provider. [Installation](docs/INSTALL.md) -2. Select **+** for official browser sign-in. If the previous account is automatically selected, use a separate browser profile/private window for the next login. On Windows, right-click **↻** to import the current Codex login. -3. Compare the cards and select **Switch**. **Account auto-switch is Off by default; enable it explicitly in its toolbar menu.** On Linux, use the app's **Start Codex** action before working. +2. Select **+** for official browser sign-in. If the previous account is automatically selected, use a separate browser profile/private window for the next login. **Settings > Import current account** imports the current Codex login. +3. Compare the cards and select **Switch**. **Account auto-switch is Off by default; enable it explicitly in Settings.** On Linux, use **Settings > Start Codex** before working. ## Features diff --git a/docs/MODEL_PROVIDERS.md b/docs/MODEL_PROVIDERS.md index 9975263..f2df7db 100644 --- a/docs/MODEL_PROVIDERS.md +++ b/docs/MODEL_PROVIDERS.md @@ -6,10 +6,10 @@ Keep eligible OpenAI models and custom Responses models in the existing Codex mo ## Set up a provider -1. Open Models in the Windows toolbar, or Model providers in the shared desktop settings menu. +1. In1.44.0, open Model providers in the titlebar Settings gear. Older1.43.3 Windows packages use Models in the toolbar. 2. Select Add a provider, enter a display name, API base URL and API key, then choose Discover and save. 3. If the provider has a large catalog, enter the model IDs to include, one per line. Leave this field empty to discover all models within the supported bounds. -4. Choose Connect Codex, then use Restart Codex on the main window when ready. Connecting prepares settings; the separate restart action normally closes and reopens Codex and resumes eligible unfinished chats with the same login. +4. Choose Connect Codex, then use Restart Codex in Settings when ready (on the main window in older1.43.3). Connecting prepares settings; the separate restart action normally closes and reopens Codex and resumes eligible unfinished chats with the same login. 5. Keep Switch running, including in its tray. Choose OpenAI or a custom model inside each Codex chat. Model-list refresh can take a few minutes after registering models. Select an existing provider to edit it. An empty key field retains its saved key; changing the destination URL requires a replacement key. Keys remain masked in the editor and encrypted in the separate local model-providers.vault file. The account vault and original OpenAI login are preserved. diff --git a/docs/README.ja.md b/docs/README.ja.md index 9479936..9d1ba34 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -3,15 +3,17 @@ **OpenAI Codexのアカウント切替と使用量を一画面で管理するデスクトップアプリです。** 各アカウントの残量を比較して次のアカウントを選択し、中断した切替は保存された記録から復旧できます。任意の残量基準による自動切替は初期状態で**オフ**です。 -Windows · macOS15以降 · Linuxプレビュー · MITオープンソース · **1.43.3** +Windows · macOS15以降 · Linuxプレビュー · MITオープンソース · **1.44.0** [English](../README.md) / [한국어](README.ko.md) / 日本語 / [简体中文](README.zh-CN.md) -[ダウンロード](#ダウンロード) · [使い始める](INSTALL.md) · [更新内容](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +[ダウンロード](#ダウンロード) · [使い始める](INSTALL.md) · [更新内容](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) -

アカウント残量、CPU・メモリ・GPU使用率と2台のストレージを表示するCodex Switch英語UIデモ

+1.44.0では残量タンクと直接のアカウント切替を保ち、モデル、自動切替、再起動、言語と外観を一つの設定メニューにまとめました。画像の多い会話の中継と同時リクエスト処理も改善しています。[変更内容と互換性](development/RELEASE_1_44_0.md)。 -アカウントとシステム情報を模擬入力したWindows英語UIデモです。[デモの出典](media/discovery/README.md) / [ネイティブ検証](evidence/account-automation/README.md)。認証情報はローカルで暗号化保存され、**アカウント切替時にCodexが再起動します。** OpenAIとは提携していない独立プロジェクトです。 +

残量タンクと一つの設定メニューを備えたCodex Switch1.44.0

+ +架空のアカウントで検証した実際のWindowsアプリ画面です。[元の画面とソース検証](evidence/home-simplification/README.md)。認証情報はローカルで暗号化保存され、**アカウント切替時にCodexが再起動します。** OpenAIとは提携していない独立プロジェクトです。 **役に立ったら、このリポジトリにStarを付けたり、他のCodexユーザーに紹介してください。** @@ -37,9 +39,9 @@ Windows · macOS15以降 · Linuxプレビュー · MITオープンソース · ## ダウンロード -- [Windows x64 ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-win-x64.zip) -- macOS15以降:[Apple Silicon ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-arm64.zip) / [Intel ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-x64.zip) -- Linux **プレビュー** x64・ARM64:[DEB・RPM・tar.gzとSHA256](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +- [Windows x64 ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-win-x64.zip) +- macOS15以降:[Apple Silicon ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-arm64.zip) / [Intel ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-x64.zip) +- Linux **プレビュー** x64・ARM64:[DEB・RPM・tar.gzとSHA256](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) .NETを同梱しており、ビルドやランタイムのインストールは不要です。フォルダー・アプリ全体を保持してください。Windows/Linuxは未署名、Macはアドホック署名で**Apple未公証**です。[インストール](INSTALL.md) / [Mac初回起動](MACOS.md) @@ -49,11 +51,11 @@ Linuxは統合プレビューです。キーストア・インストール済み 警告にホバーすると原因を表示し、押すと対象アカウントでブラウザーに再ログインできます。[実際のアプリ検証](evidence/account-automation/README.md)。 -**Linux1.43.3の配布物には、ソース1.41.0で追加した端末のアカウント切り替えと保存済みCLIセッションの再開が含まれます。** `CodexSwitch cli`で画面サーバーなしに既存の暗号化アカウントを管理し、`cli switch --resume `で手動切り替えと公式CLIの再開を実行します。`--continue`は追加メッセージ送信を明示し、`--exec --continue`は非対話実行です。検証済みのネイティブCLI・チェックサムと、ロック解除済みの対応Secret Serviceが必要です。Linux x64で実際の2アカウント切り替え後に元の文脈を保持したCLI再開を確認しました。ARM64・オフライン検証と完全なデスクトップ対応は別の範囲です。[コマンド・実際の画像・検証範囲](development/LINUX_TERMINAL.md)。 +**Linux1.44.0の配布物には、ソース1.41.0で追加した端末のアカウント切り替えと保存済みCLIセッションの再開が含まれます。** `CodexSwitch cli`で画面サーバーなしに既存の暗号化アカウントを管理し、`cli switch --resume `で手動切り替えと公式CLIの再開を実行します。`--continue`は追加メッセージ送信を明示し、`--exec --continue`は非対話実行です。検証済みのネイティブCLI・チェックサムと、ロック解除済みの対応Secret Serviceが必要です。Linux x64で実際の2アカウント切り替え後に元の文脈を保持したCLI再開を確認しました。ARM64・オフライン検証と完全なデスクトップ対応は別の範囲です。[コマンド・実際の画像・検証範囲](development/LINUX_TERMINAL.md)。 1. Windowsは`CodexSwitch.exe`、Macは`Install.command`でインストールした**Codex Switch.app**を開きます。Linuxは完全なプレビューパッケージと対応するSecret Serviceが必要です。 -2. **+**で公式ブラウザー画面から各アカウントをログインします。以前のアカウントが自動選択される場合は、別のブラウザープロファイル・プライベートウィンドウを使います。Windowsは**↻**の右クリックで現在のログインを取り込めます。 -3. 残量を比較して**Switch**を押します。**アカウント自動切替は初期状態オフで、上部メニューから有効にできます。** Linuxでは作業前に本アプリの**Start Codex**を使います。 +2. **+**で公式ブラウザー画面から各アカウントをログインします。以前のアカウントが自動選択される場合は、別のブラウザープロファイル・プライベートウィンドウを使います。**設定 > 現在のアカウントを取り込む**で現在のログインを取り込めます。 +3. 残量を比較して**Switch**を押します。**アカウント自動切替は初期状態オフで、設定から有効にできます。** Linuxでは作業前に**設定 > Start Codex**を使います。 - 日付付きチャット名を保存・検証してログインをバックアップし、Codexを正常終了します。同じチャットID・フォルダー・モデル・権限を開いて、再開可能な未完了の待機中チャットに**Continueを一度だけ**送り、自動再開します。実行中・完了済みの作業は重複実行しません。 - 暗号化された進行状況を**Resume chats**で再開できます。送信結果が不明なら自動再送せず、名前保存・正常終了の失敗時はログイン変更を停止します。Windowsでは子プロセス終了を待ち、一度のSwitchで処理を進めます。[切り替えの詳細](SWITCHING.md) diff --git a/docs/README.ko.md b/docs/README.ko.md index c879671..1821780 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -3,18 +3,20 @@ **OpenAI Codex 계정 전환과 사용량을 한 화면에서 관리하는 데스크톱 앱입니다.** 여러 계정의 잔여량을 비교하고 다음 계정을 선택하세요. 전환이 중단되면 저장된 기록으로 복구할 수 있습니다. 선택적 사용량 기준 자동 전환은 기본 **꺼짐**입니다. -Windows · macOS15+ · Linux 미리보기 · MIT 오픈소스 · **1.43.3** +Windows · macOS15+ · Linux 미리보기 · MIT 오픈소스 · **1.44.0** [English](../README.md) / 한국어 / [日本語](README.ja.md) / [简体中文](README.zh-CN.md) -[다운로드](#다운로드) · [시작하기](INSTALL.md) · [업데이트 내용](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +[다운로드](#다운로드) · [시작하기](INSTALL.md) · [업데이트 내용](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) -

계정별 잔여량, CPU·메모리·GPU 사용률과 저장장치 2개를 보여주는 Codex Switch UI 데모

+

잔량 탱크와 톱니 설정 하나로 정리한 Codex Switch1.44.0

-계정·시스템 정보를 가상으로 입력한 Windows UI 데모입니다. [데모 출처](media/discovery/README.md) / [네이티브 검증](evidence/account-automation/README.md). 인증정보는 로컬 암호화 저장소에 보관되며 **계정 전환 시 Codex가 재시작됩니다.** OpenAI와 제휴하지 않은 독립 프로젝트입니다. +가상 계정으로 검증한 실제 Windows 앱 화면입니다. [원본 화면과 소스 검증](evidence/home-simplification/README.md). 인증정보는 로컬 암호화 저장소에 보관되며 **계정 전환 시 Codex가 재시작됩니다.** OpenAI와 제휴하지 않은 독립 프로젝트입니다. **유용하다면 GitHub Star로 저장하고 다른 Codex 사용자에게도 알려주세요.** +1.44.0은 잔량 탱크와 계정별 전환을 유지하면서 모델, 자동 전환, 재시작, 언어와 화면 설정을 톱니 메뉴 하나로 모았습니다. 이미지가 많은 긴 대화의 중계 오류와 동시 요청 처리도 개선했습니다. [변경 내용과 호환성](development/RELEASE_1_44_0.md). + 1.42.2에는 경고 버튼의 해당 계정 브라우저 재로그인, 기본 꺼짐인 계정 자동 전환, 최적 자동 선택/사용자 지정 순위, 재시작 복구 기록, 구독 날짜 제목과 공식 Pro 100/200/500 표기가 포함됩니다. [동작과 검증 범위](development/ACCOUNT_AUTOMATION.md) / [릴리즈 절차](development/RELEASE_1_42_2.md). 1.42.3은 Codex 업데이트로 실행 파일이 교체된 뒤에도 Switch 재시작 없이 사용량 조회를 복구합니다. 유효한 경로는 캐시하고, 설치 중의 실패는 명시적으로 표시하며 요청을 반복 전송하지 않습니다. [복구와 검증](development/RELEASE_1_42_3.md). @@ -37,9 +39,9 @@ Windows · macOS15+ · Linux 미리보기 · MIT 오픈소스 · **1.43.3** ## 다운로드 -- [Windows x64 ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-win-x64.zip) -- macOS15+: [Apple Silicon ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-arm64.zip) / [Intel ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-x64.zip) -- Linux **미리보기** x64·ARM64: [DEB·RPM·tar.gz 및 SHA256](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +- [Windows x64 ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-win-x64.zip) +- macOS15+: [Apple Silicon ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-arm64.zip) / [Intel ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-x64.zip) +- Linux **미리보기** x64·ARM64: [DEB·RPM·tar.gz 및 SHA256](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) 전체 실행 패키지에 .NET 런타임이 포함되어 소스 빌드나 .NET 설치가 필요 없습니다. 폴더·앱 번들 전체를 유지하세요. Windows·Linux는 미서명이며 Mac은 임시 서명으로 **Apple 공증은 없습니다**. [설치](INSTALL.md) / [Mac 최초 실행](MACOS.md) @@ -49,11 +51,11 @@ Linux는 통합 미리보기입니다. 키 모음·설치 UI·패키지 검증 경고 아이콘에 마우스를 올리면 원인을 보고, 누르면 해당 계정으로 브라우저에서 다시 로그인할 수 있습니다. [실제 앱 검증 화면](evidence/account-automation/README.md). -**Linux1.43.3 다운로드에는 소스1.41.0에서 추가한 CLI 계정 전환과 저장 세션 재개가 포함됩니다.** `CodexSwitch cli`로 화면 서버 없이 기존 암호화 계정을 관리하고, `cli switch <계정> --resume `로 수동 전환과 공식 CLI 재개를 연결합니다. `--continue`는 후속 메시지 전송을 명시하며, `--exec --continue`는 비대화형 실행입니다. 검증된 네이티브 CLI·체크섬과 잠금 해제된 지원 Secret Service가 필요합니다. Linux x64에서 실제 두 계정 전환 후 기존 문맥을 기억하는 CLI 재개를 확인했습니다. ARM64·오프라인 검증과 전체 데스크톱 수용 범위는 구분합니다. [명령·실제 스크린샷·정확한 검증 범위](development/LINUX_TERMINAL.md). +**Linux1.44.0 다운로드에는 소스1.41.0에서 추가한 CLI 계정 전환과 저장 세션 재개가 포함됩니다.** `CodexSwitch cli`로 화면 서버 없이 기존 암호화 계정을 관리하고, `cli switch <계정> --resume `로 수동 전환과 공식 CLI 재개를 연결합니다. `--continue`는 후속 메시지 전송을 명시하며, `--exec --continue`는 비대화형 실행입니다. 검증된 네이티브 CLI·체크섬과 잠금 해제된 지원 Secret Service가 필요합니다. Linux x64에서 실제 두 계정 전환 후 기존 문맥을 기억하는 CLI 재개를 확인했습니다. ARM64·오프라인 검증과 전체 데스크톱 수용 범위는 구분합니다. [명령·실제 스크린샷·정확한 검증 범위](development/LINUX_TERMINAL.md). 1. Windows는 `CodexSwitch.exe`, Mac은 `Install.command`로 설치한 **Codex Switch.app**을 실행하세요. Linux는 전체 미리보기 패키지와 지원하는 Secret Service 제공자가 필요합니다. -2. **+**로 각 계정을 공식 브라우저 로그인합니다. 이전 계정이 자동 선택되면 별도 브라우저 프로필·비공개 창으로 다음 계정을 로그인하세요. Windows는 **↻** 우클릭으로 현재 로그인을 가져올 수 있습니다. -3. 잔량을 비교한 뒤 **Switch**를 누르세요. **계정 자동 전환은 기본 꺼짐이며 상단 메뉴에서 직접 켤 수 있습니다.** Linux에서는 작업 전에 앱의 **Start Codex**로 시작하세요. +2. **+**로 각 계정을 공식 브라우저 로그인합니다. 이전 계정이 자동 선택되면 별도 브라우저 프로필·비공개 창으로 다음 계정을 로그인하세요. **설정 > 현재 계정 가져오기**로 현재 로그인을 가져올 수 있습니다. +3. 잔량을 비교한 뒤 **Switch**를 누르세요. **계정 자동 전환은 기본 꺼짐이며 설정에서 직접 켤 수 있습니다.** Linux에서는 작업 전에 **설정 > Start Codex**로 시작하세요. - **수동 전환·자동 재개:** 날짜를 붙인 채팅 이름을 저장·확인하고 기존 로그인을 백업합니다. Codex를 정상 종료하고 선택 계정을 적용한 뒤 같은 채팅 ID·폴더·모델·권한을 열어, 재개 가능한 미완료 idle 채팅에 **Continue**를 한 번 보냅니다. 실행 중·완료된 작업은 중복 실행하지 않습니다. - **중단 복구:** 암호화한 진행 상태를 **Resume chats**로 이어갑니다. 전송 결과가 불확실하면 자동 재전송하지 않으며 이름 저장·정상 종료 실패 시 로그인 변경을 중단합니다. Windows 자식 프로세스 종료까지 기다려 한 번의 Switch로 진행합니다. [전환 원리](SWITCHING.md) diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 8e86325..5960d45 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -3,15 +3,17 @@ **在一个桌面界面中管理OpenAI Codex账号切换和使用额度。** 比较各账号的剩余额度、选择下一个账号,并通过已保存的记录恢复中断的切换。可选的额度阈值自动切换默认**关闭**。 -Windows · macOS15及以上 · Linux预览版 · MIT开源 · **1.43.3** +Windows · macOS15及以上 · Linux预览版 · MIT开源 · **1.44.0** [English](../README.md) / [한국어](README.ko.md) / [日本語](README.ja.md) / 简体中文 -[下载](#下载) · [开始使用](INSTALL.md) · [更新内容](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +[下载](#下载) · [开始使用](INSTALL.md) · [更新内容](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) -

Codex Switch英文UI演示:账号剩余额度、CPU与内存及GPU使用率和两个存储设备

+1.44.0保留额度水位卡片和直接账号切换,将模型、自动切换、重启、语言与外观集中到一个设置菜单中。同时改进了含多张图片的会话转发与并发请求处理。[更新内容与兼容范围](development/RELEASE_1_44_0.md)。 -使用模拟账号和系统数据的Windows英文UI演示。[演示来源](media/discovery/README.md) / [原生验证](evidence/account-automation/README.md)。凭证保存在本地加密存储中,**切换账号会重启Codex。** 本项目独立开发,与OpenAI没有隶属关系。 +

Codex Switch1.44.0额度水位卡片与统一设置菜单

+ +使用模拟账号验证的实际Windows应用界面。[原始截图与源码验证](evidence/home-simplification/README.md)。凭证保存在本地加密存储中,**切换账号会重启Codex。** 本项目独立开发,与OpenAI没有隶属关系。 **如果觉得有用,欢迎Star本仓库并分享给其他Codex用户。** @@ -37,9 +39,9 @@ Windows · macOS15及以上 · Linux预览版 · MIT开源 · **1.43.3** ## 下载 -- [Windows x64 ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-win-x64.zip) -- macOS15及以上:[Apple Silicon ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-arm64.zip) / [Intel ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.43.3/CodexSwitch-v1.43.3-osx-x64.zip) -- Linux **预览版** x64、ARM64:[DEB、RPM、tar.gz及SHA256](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) +- [Windows x64 ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-win-x64.zip) +- macOS15及以上:[Apple Silicon ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-arm64.zip) / [Intel ZIP](https://github.com/Gaebabi/codex-switch/releases/download/v1.44.0/CodexSwitch-v1.44.0-osx-x64.zip) +- Linux **预览版** x64、ARM64:[DEB、RPM、tar.gz及SHA256](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) 完整程序包包含.NET运行时,无需构建或安装.NET。请保留完整文件夹或应用包。Windows/Linux未签名;Mac为临时签名,**未经Apple公证**。[安装](INSTALL.md) / [Mac首次启动](MACOS.md) @@ -49,11 +51,11 @@ Linux仍为集成预览版。原生密钥环、已安装UI和程序包验证, 悬停警告图标可查看原因,点击可在浏览器中重新登录指定账号。[实际应用验证](evidence/account-automation/README.md)。 -**Linux1.43.3下载包含源码1.41.0新增的终端账号切换和已保存CLI会话恢复功能。** `CodexSwitch cli`无需显示服务器即可管理现有加密账号;`cli switch --resume `将手动切换与官方CLI恢复连接起来。`--continue`明确请求发送后续消息,`--exec --continue`选择非交互执行。需要经过验证的原生CLI、校验和以及已解锁的受支持Secret Service。已在Linux x64上验证真实双账号切换后保留原有上下文的CLI恢复。ARM64、离线验证及完整桌面支持仍分别记录。[命令、实际截图与验证范围](development/LINUX_TERMINAL.md)。 +**Linux1.44.0下载包含源码1.41.0新增的终端账号切换和已保存CLI会话恢复功能。** `CodexSwitch cli`无需显示服务器即可管理现有加密账号;`cli switch --resume `将手动切换与官方CLI恢复连接起来。`--continue`明确请求发送后续消息,`--exec --continue`选择非交互执行。需要经过验证的原生CLI、校验和以及已解锁的受支持Secret Service。已在Linux x64上验证真实双账号切换后保留原有上下文的CLI恢复。ARM64、离线验证及完整桌面支持仍分别记录。[命令、实际截图与验证范围](development/LINUX_TERMINAL.md)。 1. Windows运行`CodexSwitch.exe`;Mac运行`Install.command`后打开已安装的**Codex Switch.app**。Linux需要完整预览包及受支持的Secret Service。 -2. **+**通过官方浏览器页面登录各账号。如果自动选择了之前的账号,请使用独立的浏览器配置或隐私窗口。Windows可右键点击**↻**导入当前登录。 -3. 比较额度后点击**Switch**。**账号自动切换默认关闭,可从上方菜单手动开启。** Linux用户请先通过本工具的**Start Codex**启动Codex。 +2. **+**通过官方浏览器页面登录各账号。如果自动选择了之前的账号,请使用独立的浏览器配置或隐私窗口。通过**设置 > 导入当前账号**可导入当前登录。 +3. 比较额度后点击**Switch**。**账号自动切换默认关闭,可在设置中手动开启。** Linux用户请先通过**设置 > Start Codex**启动Codex。 - 保存并验证带日期的聊天名称、备份当前登录,正常退出Codex后应用所选账号。重新打开相同聊天ID、目录、模型及权限,向符合条件的未完成空闲聊天**发送一次Continue**,自动继续工作。不重复运行已执行或已完成的工作。 - **Resume chats**恢复加密保存的交接进度。发送结果不明确时不会自动重发;名称保存或正常退出失败时停止登录变更。Windows等待子进程退出,使一次Switch即可继续后续流程。[切换原理](SWITCHING.md) diff --git a/docs/RELEASES.md b/docs/RELEASES.md index 0e0bb55..72c8abb 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -1,5 +1,7 @@ # Binary releases +[Release1.44.0](https://github.com/Gaebabi/codex-switch/releases/tag/v1.44.0) simplifies Home around the existing quota tanks: Add/Refresh remain visible while one Settings gear groups global actions. It includes the corrected built-in image endpoints and bounded concurrent handling for image-heavy chats. The package set remains Windows x64, macOS15+ Apple Silicon/Intel and Linux x64/ARM64 integration previews. [Verification and installation gates](development/RELEASE_1_44_0.md). Links are staged until publication and public download verification complete. + [Release1.43.3](https://github.com/Gaebabi/codex-switch/releases/tag/v1.43.3) provides complete Windows x64 and macOS15+ Apple Silicon/Intel ZIPs, plus unsigned Linux x64/ARM64 DEB/RPM/tar.gz integration previews. Keep the entire folder/app bundle. No source build or global .NET installation is required; Codex desktop/CLI is installed separately. [Downloads](../README.md#download) / [latest published Release](https://github.com/Gaebabi/codex-switch/releases/latest). Release1.43.3 introduces configurable Responses providers and per-chat model routes, a current-account restart action, clearer credit history ranges and safer handling of completed recovery receipts. Initial provider connection requires a Codex restart and a running Switch gateway. Individual providers and the actual combined desktop picker have separate outstanding compatibility checks. [Setup and limitations](MODEL_PROVIDERS.md). diff --git a/docs/development/HOME_SIMPLIFICATION.md b/docs/development/HOME_SIMPLIFICATION.md new file mode 100644 index 0000000..c58e935 --- /dev/null +++ b/docs/development/HOME_SIMPLIFICATION.md @@ -0,0 +1,21 @@ +# Home simplification + +Source1.44.0 preserves the tank-fill metaphor, active-account wave, large quota percentage, account grid and direct per-account Switch/Recover actions. It changes visual hierarchy and global action placement only. Native verification, published downloads and installed acceptance are separate [release gates](RELEASE_1_44_0.md). + +## Global actions + +The titlebar has Add account, Refresh and one Settings gear, alongside ordinary window controls. Settings contains model providers, automatic switching and credit protection, Restart Codex, language, dark/light appearance, current-account import and drive settings. Shared desktop hosts also retain their existing Quit and Linux Start Codex actions inside Settings. + +Automatic switching retains its existing persisted preferences, thresholds, priority and credit-use controls. Its enabled state stays visible on Home as noninteractive text; its Off/On state is always named in Settings. Restart/model actions retain their busy guards. No account, vault, provider routing or switch coordinator behavior is changed. + +## Account cards + +Tank level, active wave, quota selection, percentage, reset countdown, plan, reset credits, balance and per-account actions remain. Secondary values use normal weight and muted text, with warning colors retained for low quota, stale data, usage limits and near deadlines. Unknown subscription dates do not create a repeated empty date row; the account detail still explains unavailable subscription data. Routine registered/current state text is omitted where the account action already conveys it. Login warnings remain actionable in the account header. + +Four accounts keep the existing grid and fit the500×360minimum. Larger Windows account collections use vertical overflow with a126pxminimum row, so eight accounts remain readable and the last cards can be reached. Shared desktop hosts retain their existing row/scroll policy. + +## Evidence scope + +The local Windows production-control renderer exports before/after previews in English/Korean, dark/light, and800×560/600×400/500×360sizes. It never shows or activates a window and uses fictional accounts/resources. These are production WPF component renders, not native window screenshots or owner account acceptance. [Images, native evidence and checks](../evidence/home-simplification/README.md). + +The provider-editor/home UI harness passes54checks, including minimum-width Settings reachability, retained menu entries, real language/theme/automation persistence in disposable storage, busy restart refusal,0/1/4/8accounts, long names, warnings, enabled-state visibility and scroll access. Hosted Windows and both Mac architectures pass actual application/menu checks, including Settings keyboard focus and language overflow with a visible scrollbar. [Original Windows before/after and Mac menu screenshots](../evidence/home-simplification/README.md) include exact-source/artifact hashes. Linux ordinary installed UI must exercise Add/cancel and Settings > Start Codex/appearance through the new paths, retaining the original encrypted-data, child ownership, rendering and lifecycle assertions. Final-head six-job verification, packages, public downloads and owner installation remain distinct release gates. diff --git a/docs/development/OPERATIONS.md b/docs/development/OPERATIONS.md index 4ba30e5..55f1367 100644 --- a/docs/development/OPERATIONS.md +++ b/docs/development/OPERATIONS.md @@ -1,5 +1,9 @@ # Operations +Source1.44.0 keeps Add account and Refresh beside one titlebar Settings gear. Model providers, automatic/credit controls, Restart Codex, language, appearance and drive settings are in that menu. Direct per-account Switch/Recover and warning actions stay on the tanks. [Home contract](HOME_SIMPLIFICATION.md) / [release and installation gates](RELEASE_1_44_0.md). + +The ordinary Linux desktop-session harness follows the new three-action titlebar and Settings > Start Codex/appearance using real input. Preserve its login/cancel, approved child ownership, rendered theme, original ciphertext and normal close/reopen assertions. The Settings menu ends with Start Codex when available, then Quit; the current-account import and drive entries remain above them. A stale toolbar coordinate is a test-path failure, not evidence of product success. + Built-in image generation requires `images/generations` and `images/edits` in addition to Responses/model discovery. The gateway forwards these JSON requests only with the original OpenAI identity and preserves the body and safe request IDs; custom text aliases and saved custom-provider keys do not select the image route. Browser/loopback authentication, redirect refusal and no-retry behavior still apply. Conversation and image request bodies remain bounded at512MiB, including chunked bodies. The gateway admits eight large/unknown-length uploads within16 upload/routing slots and a1GiB aggregate request-buffer budget.64MiB stays available for small requests; unknown-length growth counts both old/new arrays before resizing. Uploads time out after two minutes, and completed uploads release their buffers/slots at upstream response headers. Up to48 responses stream independently. Bounded overload responses include503/Retry-After; no internal generation retry is added. Disconnected clients cannot fault the listener's accept loop. [Load verification](../evidence/gateway-load/README.md). @@ -85,7 +89,7 @@ PR65's shared history verification uses the published native Mac app's `--verify Release1.40.2 uses [the patch release checklist](RELEASE_1_40_2.md), retaining [the completed1.40.1 publication receipt](RELEASE_1_40_1.md): native Windows/Mac build/test/package artifacts, Linux three-format previews, exact-source receipt/checksum review, inspected actual app images, author review/expected-SHA merge and manually verified draft-to-public upload. CI remains read-only and receives no owner credentials. MacBook real-device screenshot remains an owner-provided follow-up; reserve its gallery slot and mention this explicitly in the documentation commit message. -Public `Gaebabi/codex-switch`, baseline `main`, is the canonical source. Clone into any directory. Source/app version is **1.43.3**; [latest published binaries](https://github.com/Gaebabi/codex-switch/releases/latest) and [versioned downloads/verification](../RELEASES.md) provide complete Windows/macOS ZIPs, unsigned Linux preview DEB/RPM/tar.gz packages, `SHA256SUMS.txt` and source-bound `VERIFICATION.json`. No automatic updater or CI release publisher exists. [Linux packaging](LINUX_PACKAGES.md) remains an integration preview with `supportedAccountHandoff: false`; publication does not complete authenticated active-chat/continuation acceptance. +Public `Gaebabi/codex-switch`, baseline `main`, is the canonical source. Clone into any directory. Source/app version is **1.44.0**; [latest published binaries](https://github.com/Gaebabi/codex-switch/releases/latest) and [versioned downloads/verification](../RELEASES.md) provide complete Windows/macOS ZIPs, unsigned Linux preview DEB/RPM/tar.gz packages, `SHA256SUMS.txt` and source-bound `VERIFICATION.json`. No automatic updater or CI release publisher exists. [Linux packaging](LINUX_PACKAGES.md) remains an integration preview with `supportedAccountHandoff: false`; publication does not complete authenticated active-chat/continuation acceptance. Issue81 moves the shared desktop clock into the normal window lifetime so a failed initial keyring/client load cannot prevent all scheduled retries. The existing60-second schedule calls the same guarded initialization/refresh path as the ordinary Refresh action. Synthetic capture windows do not start that clock; account controls remain gated until actual initialization succeeds. The native ordinary-session test uses an actual locked/unlocked disposable keyring and no app input to verify recovery. diff --git a/docs/development/PLAN.md b/docs/development/PLAN.md index ad83248..e998e89 100644 --- a/docs/development/PLAN.md +++ b/docs/development/PLAN.md @@ -1,5 +1,7 @@ # Current plan +Issue151 delivers [Home simplification](HOME_SIMPLIFICATION.md) and the already merged image/concurrent-load gateway corrections as1.44.0. Preserve account tanks, large quota numbers, the grid and direct switching; consolidate global actions behind the titlebar gear while retaining Add/Refresh, automatic-state visibility and warning/recovery guards. Complete native application evidence, six protected checks, review, merge, package/public-download verification and separately authorized state-preserving installation. [Release gates](RELEASE_1_44_0.md). + Issue149 strengthens concurrent gateway load with an aggregate request-buffer budget, separate upload/stream limits, sliced custom rewrites and token-based validation. Verify exact512MiB bodies, eight65MiB custom uploads,48open response streams, overload/disconnect recovery and retained-memory measurements. Require final-head protected/native CI and privacy review before repository integration. Local installation and actual provider/account acceptance remain separate. Correct the remaining64MiB intermediary refusal for image-heavy conversations with a512MiB bounded body reader, limited concurrent large buffers and original-buffer forwarding. Verify declared and chunked requests, exact bounds, cancellation and unchanged history/authentication before local installation and authorized chat resumption. Public delivery remains a separate evidence state. diff --git a/docs/development/RELEASE_1_44_0.md b/docs/development/RELEASE_1_44_0.md new file mode 100644 index 0000000..775a9e0 --- /dev/null +++ b/docs/development/RELEASE_1_44_0.md @@ -0,0 +1,9 @@ +# Release 1.44.0 + +Deliver the owner-approved Home simplification and the image/large-request gateway corrections merged after1.43.3. Preserve the tank-fill and active wave, large quota percentage, account grid and direct Switch/Recover actions. Add account and Refresh stay in the titlebar; one Settings gear owns model providers, automatic/credit controls, Restart Codex, language, appearance and drive settings. Existing accounts, native protection identities, provider keys, connection settings and handoff guards are unchanged. [Home behavior](HOME_SIMPLIFICATION.md) / [gateway limits](../MODEL_PROVIDERS.md). + +Before promotion, require actual native Windows/Mac/Linux app/menu checks with fictional accounts, before/after screenshots, all six protected final-head checks, recorded review and normal expected-head merge. Verify the reviewed and merged tree, all nine packages, runtime10.0.13, original licenses/notices, payload hashes/modes, Mac staged signing/install and source provenance. Windowless component renders remain labeled and do not substitute for native app screenshots. + +Stage a draft against the verified merged tree. Verify all uploaded sizes/digests before publication and all anonymous downloads/checksums afterward. Preserve past releases and historical notes. Windows/Linux are unsigned, Mac is ad-hoc signed and not notarized, and Linux remains an integration preview with supportedAccountHandoff:false. Broader external-provider/desktop-picker/controller and Linux acceptance issues retain their existing scope. + +The owner also authorized local Windows installation. Verify the running Switch identity and zero active/queued gateway requests immediately before a normal IPC shutdown; replace only Switch, start hidden, preserve all existing data/providers/configuration and live Codex processes, then verify the loaded published package and shortcuts. Never force Codex/account operations to manufacture an idle window or acceptance result. diff --git a/docs/development/STATUS.md b/docs/development/STATUS.md index 59245d3..6a1f566 100644 --- a/docs/development/STATUS.md +++ b/docs/development/STATUS.md @@ -1,5 +1,7 @@ # Current status +Issue151/source1.44.0 consolidates global Home controls in Settings and reduces secondary metadata emphasis while preserving the tank concept and direct Switch/Recover actions. The windowless WPF harness passes54checks with disposable data. Native popup/keyboard checks, final protected CI, actual application evidence, review/merge, public packages and owner-authorized installation are in progress and remain separate gates. [Home scope](HOME_SIMPLIFICATION.md) / [release checklist](RELEASE_1_44_0.md). + Issue149's implementation passes184local official-client/provider checks,74separate generated-byte stress checks and99Windows package self-tests. It bounds request arrays at1GiB with small-request headroom, allows eight large uploads and48response streams, and forwards custom history/image slices without a full mutable JSON copy. Exact512MiB Responses/compaction/image-edit and eight simultaneous65MiB custom uploads succeed; token validation removes payload-sized pooled metadata retention. Actual hosted Windows before/after images confirm app continuity. Protected final-head CI, installed-process state and external-provider acceptance are separate gates. [Measurements, evidence and delivery scope](../evidence/gateway-load/README.md). The gateway payload-budget follow-up is in local verification: the64MiB limit still interrupted an image-heavy chat, so source now accepts bounded512MiB bodies and limits concurrent large buffers to two. Unchanged OpenAI inputs reuse the owned request buffer without a mutable JSON tree or ToArray copy. Regression, installed package and actual chat resumption evidence are recorded separately; the earlier109/99 results below do not validate this follow-up. diff --git a/docs/evidence/home-simplification/README.md b/docs/evidence/home-simplification/README.md new file mode 100644 index 0000000..039b355 --- /dev/null +++ b/docs/evidence/home-simplification/README.md @@ -0,0 +1,33 @@ +# Home simplification evidence + +The core account tanks, percentage display and direct Switch/Recover actions are preserved. Global actions move into one titlebar gear; Add account and Refresh remain beside it. Secondary metadata is quieter and missing subscription dates no longer add empty rows. [Behavior and limits](../../development/HOME_SIMPLIFICATION.md). + +## Actual application windows and menus + +| Before: Windows1.43.3 | After: Windows1.44.0 | +| --- | --- | +| ![Actual Windows app before with fictional accounts](native-before-windows.png) | ![Actual Windows app after with the same fictional accounts](native-after-windows.png) | + +![Actual Windows Settings popup](native-settings-windows.png) + +![Actual Windows language submenu with scrollbar](native-language-windows.png) + +![Actual macOS Intel application](native-after-mac-intel.png) + +![Actual macOS Settings popup](native-settings-mac-intel.png) + +![Actual macOS language submenu with scrollbar](native-language-mac-intel.png) + +These are original own-application window/popup captures from disposable hosted native workers, with fictional accounts and no owner desktop or credentials. [Source revisions, workflow/artifact digests, dimensions and original PNG hashes](evidence.json) bind the baseline main revision and implementation revision. Windows exports38images; each Mac architecture exports20. Settings keyboard focus, language overflow/visible scrollbars and existing account/credit/recovery checks pass on the captured implementation. Linux ordinary installed UI uses the new Add and Settings paths; its final native lifecycle result is recorded with the release verification. Native UI evidence does not establish authenticated provider or account-switch acceptance. + +## Earlier local component preview + +| Before | Local preview | +| --- | --- | +| ![Previous production WPF home with fictional profiles](before-ko-dark.png) | ![Simplified production WPF home with the same fictional profiles](after-ko-dark.png) | + +![Settings menu rendered from the production WPF menu controls](settings-ko-dark.png) + +These earlier images are windowless renders of the actual maintained WPF controls with fictional account/resource data, not native application-window screenshots. The titlebar preview's DEMO marker is supplied only by the documentation renderer. The before source is the pre-change main tree; the after was captured from the local implementation before its first commit. No real account, Codex restart, provider switch or installed package was exercised by this renderer. + +The UI harness passes54checks, including0/1/4/8accounts at two sizes in English/Korean and dark/light themes. Long names, warning actions, tank/number/metadata containment, the Settings path, persisted language/theme/automatic choices, busy restart refusal and vertical access to the last of eight accounts are covered. The standard preview renderer also exports600×400. Publication and owner installation require the separate [release gates](../../development/RELEASE_1_44_0.md). diff --git a/docs/evidence/home-simplification/after-ko-dark.png b/docs/evidence/home-simplification/after-ko-dark.png new file mode 100644 index 0000000..326ca05 Binary files /dev/null and b/docs/evidence/home-simplification/after-ko-dark.png differ diff --git a/docs/evidence/home-simplification/before-ko-dark.png b/docs/evidence/home-simplification/before-ko-dark.png new file mode 100644 index 0000000..1ca3f52 Binary files /dev/null and b/docs/evidence/home-simplification/before-ko-dark.png differ diff --git a/docs/evidence/home-simplification/evidence.json b/docs/evidence/home-simplification/evidence.json new file mode 100644 index 0000000..729f205 --- /dev/null +++ b/docs/evidence/home-simplification/evidence.json @@ -0,0 +1,159 @@ +{ + "schema": 1, + "scope": "Actual hosted Windows/macOS application windows and popups with fictional inputs. Original metadata-free pixels. Separate local component renders remain labeled; no owner account, desktop or authenticated switch acceptance.", + "screenshots": [ + { + "file": "native-before-windows.png", + "sourceFile": "en-dark-800x560.png", + "sourceRevision": "c729b6053c2c908b0d3af53634f72a9f0460f467", + "os": "Windows", + "architecture": "X64", + "appVersion": "1.43.3", + "runtime": ".NET 10.0.13", + "run": 41308254557, + "artifactId": 13084063945, + "archiveDigest": "sha256:b9a2d1e13cabc8d15f4a9272ea498654b8f9caa1a7dd1dde3bf0a487c45d0e7d", + "sha256": "320049d86e415b3991ed0525e353b855d15949d0f76f1f45bf6c66044e4b9ee8", + "width": 800, + "height": 560, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-after-windows.png", + "sourceFile": "en-dark-800x560.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "Windows", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090477846, + "archiveDigest": "sha256:c10e8583def202524b20e04a8d59913d6c61b86715f401d9caf5fdf469cd897c", + "sha256": "81e0d88a0d7290fc47c780a9604c83ed457de48cd1d614f1dbb0f0d3429cef30", + "width": 800, + "height": 560, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-after-windows-ko.png", + "sourceFile": "ko-dark-800x560.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "Windows", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090477846, + "archiveDigest": "sha256:c10e8583def202524b20e04a8d59913d6c61b86715f401d9caf5fdf469cd897c", + "sha256": "566c7e188ab560d15059b3b7b367fd2721d8ef980969680a2b8155afc59b43ae", + "width": 800, + "height": 560, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-after-windows-light.png", + "sourceFile": "en-light-800x560.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "Windows", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090477846, + "archiveDigest": "sha256:c10e8583def202524b20e04a8d59913d6c61b86715f401d9caf5fdf469cd897c", + "sha256": "9b43a41c2b9bb3d72a28642fb8b44f6ca2345cb105639628863a0e367e708836", + "width": 800, + "height": 560, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-settings-windows.png", + "sourceFile": "en-dark-settings.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "Windows", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090477846, + "archiveDigest": "sha256:c10e8583def202524b20e04a8d59913d6c61b86715f401d9caf5fdf469cd897c", + "sha256": "5890ecf5ff2ffcb1761f6c2dbcc901146ac26219d3acd4e901491b11a60643e9", + "width": 246, + "height": 271, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-language-windows.png", + "sourceFile": "en-dark-language.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "Windows", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090477846, + "archiveDigest": "sha256:c10e8583def202524b20e04a8d59913d6c61b86715f401d9caf5fdf469cd897c", + "sha256": "af602fa1d1ebaf0aa2e0434ada9e52df76a86dc65c74ce317ca3457936727961", + "width": 227, + "height": 336, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-after-mac-intel.png", + "sourceFile": "en-dark-800x600-native.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "macOS", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090022120, + "archiveDigest": "sha256:348388cf19a3a5148fc530fa8049e25492971e0df7553a8497bc8038d842f141", + "sha256": "0dff4e40a4c748db12ba671a0cbbbcf27b4405d779f5f8babc11f22864a5e280", + "width": 800, + "height": 600, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-settings-mac-intel.png", + "sourceFile": "en-dark-settings-native.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "macOS", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090022120, + "archiveDigest": "sha256:348388cf19a3a5148fc530fa8049e25492971e0df7553a8497bc8038d842f141", + "sha256": "d27ae26ee0609a682e650117a9f1060a5b39322c21a04f9f81dcb34d6fee439d", + "width": 231, + "height": 290, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + }, + { + "file": "native-language-mac-intel.png", + "sourceFile": "en-dark-language-native.png", + "sourceRevision": "de7bcdf220073f42037f17840a2b3c614ac04fad", + "os": "macOS", + "architecture": "X64", + "appVersion": "1.44.0", + "runtime": ".NET 10.0.13", + "run": 41323426014, + "artifactId": 13090022120, + "archiveDigest": "sha256:348388cf19a3a5148fc530fa8049e25492971e0df7553a8497bc8038d842f141", + "sha256": "03712c5e3cb2444e5e8f467fa2347ea1d438178d17810310fed787474d45a7d6", + "width": 223, + "height": 302, + "originalPixelsUnchanged": true, + "capture": "Native own window or popup" + } + ] +} diff --git a/docs/evidence/home-simplification/native-after-mac-intel.png b/docs/evidence/home-simplification/native-after-mac-intel.png new file mode 100644 index 0000000..24184cb Binary files /dev/null and b/docs/evidence/home-simplification/native-after-mac-intel.png differ diff --git a/docs/evidence/home-simplification/native-after-windows-ko.png b/docs/evidence/home-simplification/native-after-windows-ko.png new file mode 100644 index 0000000..ad35c4e Binary files /dev/null and b/docs/evidence/home-simplification/native-after-windows-ko.png differ diff --git a/docs/evidence/home-simplification/native-after-windows-light.png b/docs/evidence/home-simplification/native-after-windows-light.png new file mode 100644 index 0000000..b3713d4 Binary files /dev/null and b/docs/evidence/home-simplification/native-after-windows-light.png differ diff --git a/docs/evidence/home-simplification/native-after-windows.png b/docs/evidence/home-simplification/native-after-windows.png new file mode 100644 index 0000000..6cfaa62 Binary files /dev/null and b/docs/evidence/home-simplification/native-after-windows.png differ diff --git a/docs/evidence/home-simplification/native-before-windows.png b/docs/evidence/home-simplification/native-before-windows.png new file mode 100644 index 0000000..8442e9a Binary files /dev/null and b/docs/evidence/home-simplification/native-before-windows.png differ diff --git a/docs/evidence/home-simplification/native-language-mac-intel.png b/docs/evidence/home-simplification/native-language-mac-intel.png new file mode 100644 index 0000000..1bdf1cc Binary files /dev/null and b/docs/evidence/home-simplification/native-language-mac-intel.png differ diff --git a/docs/evidence/home-simplification/native-language-windows.png b/docs/evidence/home-simplification/native-language-windows.png new file mode 100644 index 0000000..0948f83 Binary files /dev/null and b/docs/evidence/home-simplification/native-language-windows.png differ diff --git a/docs/evidence/home-simplification/native-settings-mac-intel.png b/docs/evidence/home-simplification/native-settings-mac-intel.png new file mode 100644 index 0000000..b018e2c Binary files /dev/null and b/docs/evidence/home-simplification/native-settings-mac-intel.png differ diff --git a/docs/evidence/home-simplification/native-settings-windows.png b/docs/evidence/home-simplification/native-settings-windows.png new file mode 100644 index 0000000..b35f94c Binary files /dev/null and b/docs/evidence/home-simplification/native-settings-windows.png differ diff --git a/docs/evidence/home-simplification/settings-ko-dark.png b/docs/evidence/home-simplification/settings-ko-dark.png new file mode 100644 index 0000000..aa785ef Binary files /dev/null and b/docs/evidence/home-simplification/settings-ko-dark.png differ diff --git a/scripts/linux-desktop-session/verify-session.py b/scripts/linux-desktop-session/verify-session.py index a8e43b6..357f014 100644 --- a/scripts/linux-desktop-session/verify-session.py +++ b/scripts/linux-desktop-session/verify-session.py @@ -226,7 +226,8 @@ def find_app(): capture(window, pid, 'ordinary-native-frame', include_frame=True) run(['xdotool', 'windowsize', '--sync', window, '500', '360']) before_login_vault = (root / 'share/CodexAccountDesk/accounts.vault').read_bytes() - run(['xdotool', 'mousemove', '--window', window, '359', '17', 'click', '1']) + # Add/Refresh/Settings are the final three 28px titlebar actions. + run(['xdotool', 'mousemove', '--window', window, '430', '17', 'click', '1']) def login_controls(pending): raw = root / 'login-controls-private.xwd' png = root / 'login-controls-private.png' @@ -242,7 +243,9 @@ def login_controls(pending): wait_for(lambda: login_controls(False), 20) check('Ordinary Cancel restores the ready toolbar without changing existing ciphertext or creating authentication', not (ordinary_home / 'auth.json').exists() and (root / 'share/CodexAccountDesk/accounts.vault').read_bytes() == before_login_vault) run(['xdotool', 'windowsize', '--sync', window, '800', '600']) - run(['xdotool', 'mousemove', '--window', window, '631', '17', 'click', '1']) + run(['xdotool', 'mousemove', '--window', window, '786', '17', 'click', '1']) + # Settings ends with Start Codex and Quit; use its real keyboard path. + run(['xdotool', 'key', '--delay', '100', 'End', 'Up', 'Return']) def managed_client(): for process in Path('/proc').glob('[0-9]*'): try: @@ -255,7 +258,7 @@ def managed_client(): continue return None client_pid, client_window = wait_for(managed_client, 40) - check('Ordinary Start Codex button launches its own actual approved desktop child', Path('/proc/' + str(client_pid) + '/exe').resolve() == Path('/usr/lib/chatgpt/ChatGPT')) + check('Ordinary Settings > Start Codex launches its own actual approved desktop child', Path('/proc/' + str(client_pid) + '/exe').resolve() == Path('/usr/lib/chatgpt/ChatGPT')) run(['xdotool', 'windowactivate', '--sync', client_window]) active = run(['xdotool', 'getactivewindow'], capture_output=True, text=True).stdout.strip() if active != client_window: @@ -266,12 +269,14 @@ def managed_client(): wait_for(lambda: not Path('/proc/' + str(client_pid)).exists(), 40) check('Ordinary managed start keeps authentication empty and allows normal application Quit', not (ordinary_home / 'auth.json').exists()) run(['xdotool', 'windowactivate', '--sync', window]) - run(['xdotool', 'mousemove', '--window', window, '758', '17', 'click', '1']) + run(['xdotool', 'mousemove', '--window', window, '786', '17', 'click', '1']) + # From Quit: Start, import, drives, appearance; then choose Light. + run(['xdotool', 'key', '--delay', '100', 'End', 'Up', 'Up', 'Up', 'Up', 'Right', 'End', 'Return']) run(['xdotool', 'mousemove', '--window', window, '20', '80']) time.sleep(0.5) capture(window, pid, 'ordinary-light-800x600') with Image.open(results / 'ordinary-dark-800x600.png') as before, Image.open(results / 'ordinary-light-800x600.png') as after: - check('Ordinary theme button changes the rendered application theme', sum(after.convert('RGB').getpixel((5, 80))) > sum(before.convert('RGB').getpixel((5, 80))) + 200) + check('Ordinary Settings > appearance changes the rendered application theme', sum(after.convert('RGB').getpixel((5, 80))) > sum(before.convert('RGB').getpixel((5, 80))) + 200) run(['xdotool', 'key', '--window', window, 'F5']) check('Ordinary refresh keeps the installed program alive', Path('/proc/' + str(pid)).exists()) run(['xdotool', 'windowactivate', '--sync', window]) diff --git a/scripts/verify-ui-evidence.py b/scripts/verify-ui-evidence.py index 3328049..9bb5a54 100644 --- a/scripts/verify-ui-evidence.py +++ b/scripts/verify-ui-evidence.py @@ -18,18 +18,18 @@ if data['os']!=args.os or data['failures']!=0:raise SystemExit('Native UI verification did not pass') if args.os!='Windows' and data.get('nativeWindowCapture') is not True:raise SystemExit('Native own-window screenshots required') pattern=re.compile(r'(en|ko)-(dark|light)-(500x360|600x420|800x560|800x600)'+('' if args.os=='Windows' else '-native')+r'\.png') -extra=re.compile(r'en-(dark|light)-(language|history)'+('' if args.os=='Windows' else '-native')+r'\.png') +extra=re.compile(r'en-(dark|light)-(language|history|settings)'+('' if args.os=='Windows' else '-native')+r'\.png') tooltip=re.compile(r'en-(dark|light)-tooltip'+('' if args.os=='Windows' else '-native')+r'\.png') if args.os!='Linux' else None cpu=re.compile(r'en-(dark|light)-cpu-(setup|denied|available|menu|setup-compact|denied-compact)\.png') if args.os=='Windows' else None disk=re.compile(r'en-(dark|light)-disk-(normal|pressure|unknown|compact|tooltip)\.png') if args.os=='Windows' else None paths=sorted(p for p in args.source.glob('*.png') if pattern.fullmatch(p.name) or extra.fullmatch(p.name) or tooltip and tooltip.fullmatch(p.name) or cpu and cpu.fullmatch(p.name) or disk and disk.fullmatch(p.name)) -if len(paths)!=(36 if args.os=='Windows' else 12 if args.os=='Linux' else 18):raise SystemExit('Complete declared language/theme/size/menu/history/tooltip/CPU/disk evidence required') +if len(paths)!=(38 if args.os=='Windows' else 12 if args.os=='Linux' else 20):raise SystemExit('Complete declared language/theme/size/settings/menu/history/tooltip/CPU/disk evidence required') args.destination.mkdir(parents=True,exist_ok=True) images=[] for path in paths: with Image.open(path) as raw: raw.load();rgb=raw.convert('RGB') - minimum=(160,25) if '-cpu-menu' in path.name else (60,25) if '-tooltip' in path.name else (160,160) if '-language' in path.name else (400,280) + minimum=(160,25) if '-cpu-menu' in path.name else (60,25) if '-tooltip' in path.name else (160,160) if '-language' in path.name or '-settings' in path.name else (400,280) if rgb.width4000 or rgb.height>3000 or rgb.entropy()<1 or all(low==high for low,high in rgb.getextrema()): raise SystemExit('Blank or invalid native window image') clean=Image.new('RGB',rgb.size);clean.paste(rgb) diff --git a/src/desktop/AccountCard.cs b/src/desktop/AccountCard.cs index 47df198..97d5f47 100644 --- a/src/desktop/AccountCard.cs +++ b/src/desktop/AccountCard.cs @@ -60,13 +60,13 @@ public MacAccountCard(AccountProfile profile, QuotaChoice? quota, DeskTheme them Put(theme.Text(Loc.T("소진"), 10, "muted"), 0, 0); Put(theme.Text(Loc.T("리셋"), 10, "muted"), 1, 0); _depletion = theme.Text("", 11); _depletion.Margin = new Thickness(6, 0); Put(_depletion, 0, 1); _reset = theme.Text("", 11); _reset.Margin = new Thickness(6, 0); Put(_reset, 1, 1); - var plan = theme.Text(SubscriptionPresentation.Plan(profile.Plan), 11); + var plan = theme.Text(SubscriptionPresentation.Plan(profile.Plan), 11, "muted"); plan.FlowDirection = FlowDirection.LeftToRight; plan.HorizontalAlignment = HorizontalAlignment.Right; Put(plan, 0, 2); - var resets = theme.Text(profile.Usage?.AvailableResets is int count ? Loc.F("초기화권 {0}장", count) : Loc.T("초기화권 —"), 11); + var resets = theme.Text(profile.Usage?.AvailableResets is int count ? Loc.F("초기화권 {0}장", count) : Loc.T("초기화권 —"), 11, "muted"); resets.FlowDirection = FlowDirection.LeftToRight; resets.HorizontalAlignment = HorizontalAlignment.Right; Put(resets, 1, 2); _subscription = theme.Text(Subscription(profile.SubscriptionActiveUntil, DateTimeOffset.UtcNow), 10, "muted"); Grid.SetColumnSpan(_subscription, 2); Put(_subscription, 2, 0); var bucket = profile.Usage?.Buckets.FirstOrDefault(b => b.Id == quota?.BucketId) ?? profile.Usage?.Buckets.FirstOrDefault(); - var credit = theme.Text(Credits(bucket), 10); credit.FlowDirection = FlowDirection.LeftToRight; credit.HorizontalAlignment = HorizontalAlignment.Right; Put(credit, 2, 2); + var credit = theme.Text(Credits(bucket), 10, "muted"); credit.FlowDirection = FlowDirection.LeftToRight; credit.HorizontalAlignment = HorizontalAlignment.Right; Put(credit, 2, 2); ToolTip.SetTip(credit, bucket?.CreditBalance ?? (bucket?.UnlimitedCredits == true ? Loc.T("크레딧 제한 없음") : Loc.T("크레딧 정보 없음"))); ToolTip.SetTip(this, profile.Error == null ? null : Loc.F("갱신 실패 · 이전 조회값\n{0}", Loc.Error(profile.Error))); Grid.SetRow(footer, 2); layout.Children.Add(footer); Children.Add(layout); Update(DateTimeOffset.UtcNow); } @@ -74,13 +74,17 @@ public void Update(DateTimeOffset now) { _depletion.FlowDirection = FlowDirection.LeftToRight; _reset.FlowDirection = FlowDirection.LeftToRight; _subscription.FlowDirection = FlowDirection.LeftToRight; _subscription.Text = Subscription(_profile.SubscriptionActiveUntil, now); + _subscription.IsVisible = _profile.SubscriptionActiveUntil.HasValue; + ToolTip.SetTip(_subscription, _profile.SubscriptionActiveUntil.HasValue ? _subscription.Text : Loc.T("구독 종료일 정보 없음")); _subscription.Foreground = _theme.Brush(AccountDateWarnings.Subscription(_profile.SubscriptionActiveUntil, now) ? "warning" : "muted"); _reset.Foreground = _theme.Brush(AccountDateWarnings.Reset(_quota?.Window.ResetsAt, now) ? "warning" : "text"); if (_quota == null) { _depletion.Text = Loc.T(_profile.IsRefreshing ? "측정중" : "정보 없음"); _reset.Text = "—"; return; } var forecast = UsagePredictor.Evaluate(_quota.Window, _trend, now); _depletion.Text = forecast.State == ForecastState.ResetsFirst ? Loc.T("리셋 먼저") : forecast.ExhaustsAt is DateTimeOffset end ? Compact(end, now) : Loc.T(forecast.State switch { ForecastState.Collecting or ForecastState.InsufficientChange => "측정중", ForecastState.NoGrowth => "증가 없음", ForecastState.Stale => "갱신 필요", ForecastState.Exhausted => "한도 도달", _ => "갱신 대기" }); - if (_profile.Usage?.OrdinaryUsageAllowed == false || _profile.Usage?.Buckets.Any(b => b.LimitState != null || b.SpendControlReached == true) == true) _depletion.Text = Loc.T("사용 제한"); + _depletion.Foreground = _theme.Brush(forecast.State is ForecastState.Exhausted or ForecastState.Stale ? "warning" : "muted"); + if (_profile.Usage?.OrdinaryUsageAllowed == false || _profile.Usage?.Buckets.Any(b => b.LimitState != null || b.SpendControlReached == true) == true) + { _depletion.Text = Loc.T("사용 제한"); _depletion.Foreground = _theme.Brush("warning"); } ToolTip.SetTip(_depletion, UsagePresentation.Outlook(forecast) + "\n" + UsagePresentation.Basis(forecast)); _reset.Text = _quota.Window.ResetsAt is long time ? Compact(DateTimeOffset.FromUnixTimeSeconds(time), now) : "—"; ToolTip.SetTip(_reset, _quota.Window.ResetLabel + "\n" + _quota.Window.TimeUntilReset(now)); diff --git a/src/desktop/DeskButtonIcon.cs b/src/desktop/DeskButtonIcon.cs index f21901a..966a705 100644 --- a/src/desktop/DeskButtonIcon.cs +++ b/src/desktop/DeskButtonIcon.cs @@ -4,7 +4,7 @@ namespace CodexAccountDesk; -internal enum DeskButtonIconKind { More, Refresh, Plus, Up, Down, Sun, Moon, Launch } +internal enum DeskButtonIconKind { More, Refresh, Plus, Up, Down, Sun, Moon, Launch, Settings } internal sealed class DeskButtonIcon : Control { @@ -15,6 +15,7 @@ internal sealed class DeskButtonIcon : Control private static readonly Geometry Sun = Geometry.Parse("M12 2V4M12 20V22M2 12H4M20 12H22M5 5L6.5 6.5M17.5 17.5L19 19M5 19L6.5 17.5M17.5 6.5L19 5M16 12A4 4 0 1 1 8 12A4 4 0 1 1 16 12"); private static readonly Geometry Moon = Geometry.Parse("M20.5 13A8.5 8.5 0 1 1 11 3.5A6.5 6.5 0 0 0 20.5 13Z"); private static readonly Geometry Launch = Geometry.Parse("M14 4H20V10M20 4L10 14M10 5H5V20H20V15"); + private static readonly Geometry Settings = Geometry.Parse("M9 3H15L16 6L19 7L22 10V14L19 17L16 18L15 21H9L8 18L5 17L2 14V10L5 7L8 6Z M15 12A3 3 0 1 1 9 12A3 3 0 1 1 15 12"); private readonly DeskButtonIconKind _kind; private readonly IBrush _brush; public DeskButtonIcon(DeskButtonIconKind kind, IBrush brush) { _kind = kind; _brush = brush; IsHitTestVisible = false; Width = Height = 14; } @@ -28,7 +29,7 @@ public override void Render(DrawingContext context) } else context.DrawGeometry(null, new Pen(_brush, 1.8, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), _kind switch { DeskButtonIconKind.Refresh => Refresh, DeskButtonIconKind.Plus => Plus, DeskButtonIconKind.Up => Up, - DeskButtonIconKind.Down => Down, DeskButtonIconKind.Sun => Sun, DeskButtonIconKind.Launch => Launch, _ => Moon }); + DeskButtonIconKind.Down => Down, DeskButtonIconKind.Sun => Sun, DeskButtonIconKind.Launch => Launch, DeskButtonIconKind.Settings => Settings, _ => Moon }); } } } diff --git a/src/desktop/DeskTheme.cs b/src/desktop/DeskTheme.cs index b9d260c..555220f 100644 --- a/src/desktop/DeskTheme.cs +++ b/src/desktop/DeskTheme.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using Avalonia; using Avalonia.Controls; @@ -12,6 +13,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Styling; +using Avalonia.VisualTree; using AButton = Avalonia.Controls.Button; namespace CodexAccountDesk; @@ -86,6 +88,30 @@ public ContextMenu Menu(Control owner) menu.Styles.Add(new Style(selector => selector.OfType().Class(":pointerover")) { Setters = { new Setter(MenuItem.BackgroundProperty, Brush("hover")) } }); menu.Styles.Add(new Style(selector => selector.OfType().Class(":checked")) { Setters = { new Setter(MenuItem.BackgroundProperty, Brush("selected")) } }); menu.Styles.Add(new Style(selector => selector.OfType().Class(":disabled")) { Setters = { new Setter(MenuItem.OpacityProperty, 0.45) } }); + // Submenus use Fluent's own popup template rather than ContextMenu.Template. + // Bound their scroll viewport as well as the root menu, including languages. + menu.Styles.Add(new Style(selector => selector.OfType().Template().OfType()) { Setters = { + new Setter(ScrollViewer.MaxHeightProperty, 300.0), + new Setter(ScrollViewer.VerticalScrollBarVisibilityProperty, ScrollBarVisibility.Auto), + new Setter(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Disabled), + new Setter(ScrollViewer.AllowAutoHideProperty, false) } }); + menu.AddHandler(MenuItem.SubmenuOpenedEvent, (_, args) => + { + if (args.Source is not MenuItem item) return; + foreach (var popup in item.GetTemplateDescendants().OfType()) + { + if (popup.Child == null) continue; + foreach (var scroll in popup.Child.GetVisualDescendants().OfType()) + { + // Fluent sets its arrow-only theme as a local template value; + // an ordinary style setter cannot replace that local value. + scroll.Theme = null; scroll.MaxHeight = 300; + scroll.AllowAutoHide = false; + scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + } + } + }); menu.Styles.Add(new Style(selector => selector.OfType()) { Setters = { new Setter(Separator.BackgroundProperty, Brush("divider")), new Setter(Separator.MarginProperty, new Thickness(8, 5)), new Setter(Separator.HeightProperty, 1.0) } }); EventHandler detached = (_, _) => menu.Close(); diff --git a/src/desktop/DeskWindow.cs b/src/desktop/DeskWindow.cs index da8593b..77541e1 100644 --- a/src/desktop/DeskWindow.cs +++ b/src/desktop/DeskWindow.cs @@ -39,8 +39,7 @@ internal sealed class DeskWindow : Window private StackPanel _sessionValues = new(), _volumes = new(); private TextBlock _status = new(); private Button _add = new(), _cancel = new(), _refresh = new(), _settingsButton = new(), _startCodex = new(); - private Button _automaticSwitch = new(); - private Button _restartCodex = new(); + private MenuItem _restartCodex = new(); private IDesktopMenuBar? _tray; private readonly Dictionary _nativeItems = new(); private CancellationTokenSource? _login, _switch; @@ -125,7 +124,7 @@ private void Build() FlowDirection = Loc.RightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; var nativeTitle = OperatingSystem.IsLinux(); var shell = new Grid { RowDefinitions = new RowDefinitions(nativeTitle ? "34,0,*,50" : "34,Auto,*,50") }; - var titlebar = new Grid { Background = _theme.Brush("chrome"), ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto,Auto,Auto"), Margin = new Thickness(0) }; + var titlebar = new Grid { Background = _theme.Brush("chrome"), ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto"), Margin = new Thickness(0) }; var caption = _theme.Text("Codex Switch", 12); caption.VerticalAlignment = VerticalAlignment.Center; var mark = new BrandMark { Width = 18, Height = 18, Ink = _theme.Brush("text") }; var captionContent = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, VerticalAlignment = VerticalAlignment.Center }; @@ -145,29 +144,13 @@ private void Build() })); startCodex.Name = "StartCodexButton"; startCodex.Classes.Add("title-action"); startCodex.IsVisible = OperatingSystem.IsLinux() && _host.AccountUnavailableReason == null && (_service == null || _service.HasLinuxDesktop); - Grid.SetColumn(startCodex, 1); titlebar.Children.Add(startCodex); _add = _theme.IconButton(DeskButtonIconKind.Plus, Loc.T("계정 추가"), () => _ = AddAccountAsync()); - _add.Classes.Add("title-action"); Grid.SetColumn(_add, 2); titlebar.Children.Add(_add); + _add.Name = "AddAccountButton"; _add.Classes.Add("title-action"); Grid.SetColumn(_add, 1); titlebar.Children.Add(_add); _refresh = _theme.IconButton(DeskButtonIconKind.Refresh, Loc.T("새로 고침"), () => _ = RefreshAsync()); _refresh.Classes.Add("title-action"); ToolTip.SetTip(_refresh, Loc.T("새로 고침") + (_host.CommandModifier == KeyModifiers.Meta ? " (⌘R / F5)" : " (Ctrl+R / F5)")); - Grid.SetColumn(_refresh, 3); titlebar.Children.Add(_refresh); - var language = _theme.Button(Loc.LanguageCode + " ▾", () => { }); - language.Name = "LanguageButton"; - ToolTip.SetTip(language, Loc.LanguageName + " · " + Loc.T("언어 설정")); - Avalonia.Automation.AutomationProperties.SetName(language, Loc.T("언어 설정") + ": " + Loc.LanguageName); - language.Click += (_, _) => OpenLanguageMenu(language); - Grid.SetColumn(language, 4); titlebar.Children.Add(language); - var theme = _theme.IconButton(_theme.Dark ? DeskButtonIconKind.Sun : DeskButtonIconKind.Moon, Loc.T(_theme.Dark ? "라이트" : "다크"), () => - { - _ = RunAsync(_ => - { - var next = new DeskTheme(!_theme.Dark, _host.Appearance); - if (_service != null) { lock (_service.Vault.SyncRoot) { _service.Vault.Data.Theme = next.Dark ? "dark" : "light"; _service.Vault.Save(); } } - _theme = next; Build(); return Task.CompletedTask; - }); - }); theme.Name = "ThemeButton"; theme.Classes.Add("title-action"); Grid.SetColumn(theme, 5); titlebar.Children.Add(theme); - _settingsButton = _theme.IconButton(DeskButtonIconKind.More, Loc.T("설정"), OpenSettings); _settingsButton.Width = 28; _settingsButton.Padding = new Thickness(0); - Grid.SetColumn(_settingsButton, 6); titlebar.Children.Add(_settingsButton); shell.Children.Add(titlebar); + _refresh.Name = "RefreshButton"; Grid.SetColumn(_refresh, 2); titlebar.Children.Add(_refresh); + _settingsButton = _theme.IconButton(DeskButtonIconKind.Settings, Loc.T("설정"), OpenSettings); _settingsButton.Name = "SettingsButton"; _settingsButton.Width = 28; _settingsButton.Padding = new Thickness(0); + Grid.SetColumn(_settingsButton, 3); titlebar.Children.Add(_settingsButton); shell.Children.Add(titlebar); var toolbar = new Grid { Margin = new Thickness(8, 1), ColumnDefinitions = new ColumnDefinitions(nativeTitle ? "Auto,*,Auto" : "*,Auto,*") }; _cancel = _theme.Button(Loc.T("취소"), () => { _login?.Cancel(); _switch?.Cancel(); }); _cancel.IsVisible = _login != null || _switch != null; _cancel.HorizontalAlignment = HorizontalAlignment.Left; @@ -175,29 +158,17 @@ private void Build() _sessionValues = new StackPanel { Orientation = Orientation.Horizontal, Spacing = nativeTitle ? 12 : 24, HorizontalAlignment = HorizontalAlignment.Center }; Grid.SetColumn(_sessionValues, 1); toolbar.Children.Add(_sessionValues); - _automaticSwitch = _theme.Button(Loc.T("auto.title") + " ▾", OpenAutomaticSwitchMenu); - _automaticSwitch.Name = "AutomaticSwitchButton"; _automaticSwitch.HorizontalAlignment = HorizontalAlignment.Right; var automaticSwitchLabel = Loc.T(_service?.Vault.Data.AutomaticSwitchEnabled == true ? "auto.on" : "auto.off"); - _automaticSwitch.Content = new TextBlock { Text = Loc.T("auto.title") + " ▾", TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center }; - Avalonia.Automation.AutomationProperties.SetName(_automaticSwitch, automaticSwitchLabel); - _automaticSwitch.Margin = new Thickness(6, 0, 0, 0); Grid.SetColumn(_automaticSwitch, 2); - _automaticSwitch.FontSize = 10; _automaticSwitch.Padding = new Thickness(5, 1); _automaticSwitch.MinHeight = 20; - if (nativeTitle) { _automaticSwitch.MinWidth = 132; _automaticSwitch.MaxWidth = 160; } - ToolTip.SetTip(_automaticSwitch, automaticSwitchLabel + "\n" + (_service == null ? Loc.T("auto.next_unavailable") : AccountAutomation.NextLabel(_service, DateTimeOffset.UtcNow)) + "\n" + Loc.F("auto.help", _service?.Vault.Data.AutomaticSwitchThreshold ?? 10) + "\n" + - Loc.T(_service?.Vault.Data.AutomaticSwitchUsePriority == true ? "auto.custom" : "auto.optimal") + "\n" + Loc.T("auto.priority_help")); - toolbar.Children.Add(_automaticSwitch); - _restartCodex = _theme.Button(Loc.T("restart.title"), () => - { - var current = _service?.Accounts.SingleOrDefault(profile => profile.IsCurrent); - if (current != null) _ = SwitchAsync(current, restart: true); - }); - _restartCodex.Name = "RestartCodexButton"; _restartCodex.FontSize = 10; _restartCodex.Padding = new Thickness(6, 1); - ToolTip.SetTip(_restartCodex, Loc.T("restart.help")); + var automaticState = _theme.Text(automaticSwitchLabel, 10, "muted"); + automaticState.IsVisible = _service?.Vault.Data.AutomaticSwitchEnabled == true; automaticState.HorizontalAlignment = HorizontalAlignment.Right; + ToolTip.SetTip(automaticState, automaticSwitchLabel); Grid.SetColumn(automaticState, 2); toolbar.Children.Add(automaticState); _restartCodex.IsEnabled = _ready && !Exiting && _switch == null && _login == null && !_providerEditing && _service?.Accounts.Any(profile => profile.IsCurrent) == true && _service.Vault.Data.ActivePendingSwitch is not { RestartOnly: false }; _restartCodex.IsVisible = _login == null && _switch == null; if (nativeTitle) { - titlebar.Children.Add(toolbar); + // Linux already has an OS titlebar; share the free action-strip column + // with counters instead of taking another row from the tank cards. + Grid.SetColumn(toolbar, 0); titlebar.Children.Add(toolbar); } else { Grid.SetRow(toolbar, 1); shell.Children.Add(toolbar); } _accounts = new Grid { Margin = new Thickness(4, 1) }; @@ -207,7 +178,7 @@ private void Build() _hardware = new Grid { Height = 18, Margin = new Thickness(0, 0, 0, 4), FlowDirection = FlowDirection.LeftToRight }; Grid.SetColumnSpan(_hardware, 2); footer.Children.Add(_hardware); _volumes = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6, FlowDirection = FlowDirection.LeftToRight }; var footerActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; - footerActions.Children.Add(_restartCodex); footerActions.Children.Add(_volumes); Grid.SetRow(footerActions, 1); footer.Children.Add(footerActions); + footerActions.Children.Add(_volumes); Grid.SetRow(footerActions, 1); footer.Children.Add(footerActions); _status = _theme.Text(_statusMessage, 10, "muted"); ToolTip.SetTip(_status, _statusMessage); _status.MaxWidth = 150; _status.VerticalAlignment = VerticalAlignment.Center; Grid.SetColumn(_status, 1); Grid.SetRow(_status, 1); footer.Children.Add(_status); Grid.SetRow(footer, 3); shell.Children.Add(footer); Content = shell; RenderAccounts(); RenderSessions(); RenderHardware(); RenderVolumes(); @@ -219,9 +190,9 @@ private void ChangeLanguage(string code) Loc.SetLanguage(code); Build(); UpdateTray(); } - internal ContextMenu OpenLanguageMenu(Button owner) + private MenuItem BuildLanguageSettings() { - var menu = _theme.Menu(owner); + var menu = new MenuItem { Name = "LanguageSettingsMenuItem", Header = Loc.T("언어 설정") + " " + Loc.LanguageName }; foreach (var info in Loc.Languages) { var item = new MenuItem { Header = info.NativeName, ToggleType = MenuItemToggleType.Radio, IsChecked = info.Code == Loc.Language }; @@ -236,7 +207,6 @@ internal ContextMenu OpenLanguageMenu(Button owner) restore.Click += async (_, _) => { try { ChangeLanguage(Loc.RestoreBuiltIn(Path.Combine(_host.DataDirectory, "languages"))); _statusMessage = Loc.T("Local translations replaced with the built-in pack."); Build(); } catch (Exception error) { await DeskDialogs.Message(this, _theme, Loc.T("Language packs"), error.Message); } }; menu.Items.Add(restore); } - menu.Open(owner); return menu; } private async Task ImportLanguagePackAsync() @@ -298,7 +268,7 @@ internal void Preview(string language, bool dark, AccountProfile[] profiles) { Loc.SetLanguage(language); _theme = new(dark, _host.Appearance); _preview = profiles; Build(); UpdateTray(); } internal IReadOnlyList Cards => _cards; internal ScrollViewer AccountScroll => _accountScroll; - internal Button AutomaticSwitchButton => _automaticSwitch; + internal Button SettingsButton => _settingsButton; internal Control SessionValues => _sessionValues; private async Task RunAsync(Func action) { @@ -370,37 +340,38 @@ await RunAsync(async _ => } finally { if (automatic && _service!.Vault.Data.ActivePendingSwitch != null) AccountAutomation.Pause(_service); _switch.Dispose(); _switch = null; _switchTarget = null; if (!Exiting) { Build(); } } } - private void OpenAutomaticSwitchMenu() + internal ContextMenu BuildAutomaticSwitchMenu() { - if (!_ready || _service == null) return; - var data = _service.Vault.Data; - var menu = _theme.Menu(_automaticSwitch); + var service = _service; + var data = service?.Vault.Data; + var menu = _theme.Menu(_settingsButton); + if (data == null || service == null) return menu; var enabled = new ToggleSwitch { Content = Loc.T("auto.title"), IsChecked = data.AutomaticSwitchEnabled, OnContent = "", OffContent = "" }; Avalonia.Automation.AutomationProperties.SetName(enabled, Loc.T("auto.enable")); - var next = new MenuItem { Header = AccountAutomation.NextLabel(_service, DateTimeOffset.UtcNow), IsEnabled = false, Opacity = 1, Foreground = _theme.Brush("muted") }; - enabled.IsCheckedChanged += (_, _) => { lock (_service.Vault.SyncRoot) { data.AutomaticSwitchEnabled = enabled.IsChecked == true; _service.Vault.Save(); } Build(); }; + var next = new MenuItem { Header = AccountAutomation.NextLabel(service, DateTimeOffset.UtcNow), IsEnabled = false, Opacity = 1, Foreground = _theme.Brush("muted") }; + enabled.IsCheckedChanged += (_, _) => { lock (service.Vault.SyncRoot) { data.AutomaticSwitchEnabled = enabled.IsChecked == true; service.Vault.Save(); } Build(); }; var items = new List { new MenuItem { Header = enabled, StaysOpenOnClick = true }, next, new Separator() }; var credits = new ToggleSwitch { Name = "AllowCreditUse", Content = Loc.T("credits.allow"), IsChecked = data.AllowCreditUse, OnContent = "", OffContent = "" }; ToolTip.SetTip(credits, new TextBlock { Text = Loc.T("credits.allow_help"), MaxWidth = 340, TextWrapping = TextWrapping.Wrap }); - credits.IsCheckedChanged += (_, _) => { lock (_service.Vault.SyncRoot) { data.AllowCreditUse = credits.IsChecked == true; _service.Vault.Save(); } Build(); }; + credits.IsCheckedChanged += (_, _) => { lock (service.Vault.SyncRoot) { data.AllowCreditUse = credits.IsChecked == true; service.Vault.Save(); } Build(); }; items.Insert(2, new MenuItem { Header = credits, StaysOpenOnClick = true }); foreach (var custom in new[] { false, true }) { var mode = new MenuItem { Header = Loc.T(custom ? "auto.custom" : "auto.optimal"), ToggleType = MenuItemToggleType.Radio, IsChecked = data.AutomaticSwitchUsePriority == custom }; if (custom) ToolTip.SetTip(mode, Loc.T("auto.priority_help")); - mode.Click += (_, _) => { lock (_service.Vault.SyncRoot) { data.AutomaticSwitchUsePriority = custom; _service.Vault.Save(); } Build(); }; + mode.Click += (_, _) => { lock (service.Vault.SyncRoot) { data.AutomaticSwitchUsePriority = custom; service.Vault.Save(); } Build(); }; items.Add(mode); } - var order = AccountAutomation.OrderedAccounts(_service); + var order = AccountAutomation.OrderedAccounts(service); items.Add(new MenuItem { Header = Loc.T("auto.order"), ItemsSource = order.Select((p, i) => new MenuItem { Header = Loc.F("auto.ranked_account", i + 1, p.DisplayName), IsEnabled = false }).ToArray() }); items.Add(new Separator()); foreach (var value in new[] { 5, 10, 15, 20 }) { var item = new MenuItem { Header = Loc.F("auto.threshold", value), ToggleType = MenuItemToggleType.Radio, IsChecked = data.AutomaticSwitchThreshold == value }; - item.Click += (_, _) => { lock (_service.Vault.SyncRoot) { data.AutomaticSwitchThreshold = value; _service.Vault.Save(); } Build(); }; + item.Click += (_, _) => { lock (service.Vault.SyncRoot) { data.AutomaticSwitchThreshold = value; service.Vault.Save(); } Build(); }; items.Add(item); } - menu.ItemsSource = items; _automaticSwitch.ContextMenu = menu; menu.Open(_automaticSwitch); + menu.ItemsSource = items; return menu; } private async Task TryAutomaticSwitchAsync() { @@ -544,21 +515,46 @@ private void RenderVolumes() private void OpenSettings() { if (_service == null) return; + var menu = BuildSettingsMenu(); menu.Open(_settingsButton); + } + internal ContextMenu BuildSettingsMenu() + { var menu = _theme.Menu(_settingsButton); + var service = _service; var drives = new MenuItem { Header = Loc.T("드라이브 설정") }; drives.Click += async (_, _) => { - var selection = await DeskDialogs.Volumes(this, _theme, _service.Vault.Data.Drives.ToArray(), _host.RootVolumeLabel); - if (selection != null) await RunAsync(_ => { lock (_service.Vault.SyncRoot) { _service.Vault.Data.Drives = selection.ToList(); _service.Vault.Save(); } _drives.Configure(selection); return Task.CompletedTask; }); + if (service == null) return; + var selection = await DeskDialogs.Volumes(this, _theme, service.Vault.Data.Drives.ToArray(), _host.RootVolumeLabel); + if (selection != null) await RunAsync(_ => { lock (service.Vault.SyncRoot) { service.Vault.Data.Drives = selection.ToList(); service.Vault.Save(); } _drives.Configure(selection); return Task.CompletedTask; }); }; var current = new MenuItem { Header = Loc.T("현재 계정 가져오기") }; current.Click += async (_, _) => await RefreshAsync(); - var providers = new MenuItem { Header = Loc.T("providers.title") }; providers.Click += async (_, _) => + var providers = new MenuItem { Header = Loc.T("providers.title"), IsEnabled = service != null && _switch == null && _login == null }; providers.Click += async (_, _) => { if (_service == null || _switch != null || _login != null) return; try { _providerEditing = true; await RunAsync(async _ => await new DeskProvidersWindow(_theme, _service).ShowDialog(this)); } finally { _providerEditing = false; } }; var quit = new MenuItem { Header = Loc.T("종료") }; quit.Click += async (_, _) => await ExitAsync(); - menu.ItemsSource = new[] { providers, drives, current, quit }; menu.Open(_settingsButton); + var automatic = new MenuItem { Name = "AutomaticSwitchMenuItem", Header = Loc.T(service?.Vault.Data.AutomaticSwitchEnabled == true ? "auto.on" : "auto.off"), IsEnabled = service != null, ItemsSource = BuildAutomaticSwitchMenu().ItemsSource }; + var languages = BuildLanguageSettings(); + var appearance = new MenuItem { Name = "ThemeSettingsMenuItem", Header = Loc.T("다크") + " / " + Loc.T("라이트") }; + foreach (var dark in new[] { true, false }) + { + var choice = new MenuItem { Header = Loc.T(dark ? "다크" : "라이트"), ToggleType = MenuItemToggleType.Radio, IsChecked = dark == _theme.Dark }; + choice.Click += (_, _) => { if (_service != null) { lock (_service.Vault.SyncRoot) { _service.Vault.Data.Theme = dark ? "dark" : "light"; _service.Vault.Save(); } } _theme = new DeskTheme(dark, _host.Appearance); Build(); }; + appearance.Items.Add(choice); + } + _restartCodex = new MenuItem { Header = Loc.T("restart.title"), Name = "RestartCodexMenuItem", + IsEnabled = _ready && !Exiting && _switch == null && _login == null && !_providerEditing && service?.Accounts.Any(profile => profile.IsCurrent) == true && service.Vault.Data.ActivePendingSwitch is not { RestartOnly: false } }; + ToolTip.SetTip(_restartCodex, Loc.T("restart.help")); + _restartCodex.Click += (_, _) => { var currentAccount = _service?.Accounts.SingleOrDefault(profile => profile.IsCurrent); if (currentAccount != null) _ = SwitchAsync(currentAccount, restart: true); }; + var items = new List { providers, automatic, _restartCodex, new Separator(), languages, appearance, new Separator(), drives, current }; + if (_startCodex.IsVisible) + { + var start = new MenuItem { Header = Loc.T("msg.linux_start_codex"), IsEnabled = _startCodex.IsEnabled }; + start.Click += (_, _) => _ = RunAsync(async token => await _service!.StartDesktopAsync(token)); items.Add(start); + } + items.Add(quit); menu.ItemsSource = items; _settingsButton.ContextMenu = menu; return menu; } private void UpdateTray() { diff --git a/src/linux/LinuxUiEvidence.cs b/src/linux/LinuxUiEvidence.cs index 4c31fd6..53f50a0 100644 --- a/src/linux/LinuxUiEvidence.cs +++ b/src/linux/LinuxUiEvidence.cs @@ -93,13 +93,10 @@ public static async Task RenderAsync(DeskWindow window, string directory) } if (window.AccountScroll.Extent.Height > window.AccountScroll.Viewport.Height + 1) throw new InvalidOperationException("Four Linux preview cards unexpectedly require scrolling."); - var automatic = window.AutomaticSwitchButton; - if (automatic.Content is not TextBlock caption || caption.Text != Loc.T("auto.title") + " ▾" || - caption.TextLayout.Height > caption.Bounds.Height + 1 || caption.TextLayout.Width > caption.Bounds.Width + 1) - throw new InvalidOperationException("The complete automatic-switch caption/state must fit its actual text bounds."); - var captionPoint = caption.TranslatePoint(new Point(), automatic); - if (!captionPoint.HasValue || captionPoint.Value.Y < -1 || captionPoint.Value.Y + caption.Bounds.Height > automatic.Bounds.Height + 1) - throw new InvalidOperationException("The automatic-switch caption exceeds the visible button."); + var automatic = window.SettingsButton; + var settings = window.BuildSettingsMenu(); + if (!settings.ItemsSource!.Cast().OfType().Any(item => item.Name == "AutomaticSwitchMenuItem" && item.Header as string == Loc.T("auto.off"))) + throw new InvalidOperationException("The settings gear must expose the complete automatic-switch Off state."); var automaticPoint = automatic.TranslatePoint(new Point(), window) ?? throw new InvalidOperationException("Automatic-switch button is detached."); bool Overlaps(Control control) { @@ -111,7 +108,7 @@ bool Overlaps(Control control) if (automaticPoint.X < 0 || automaticPoint.X + automatic.Bounds.Width > window.ClientSize.Width + 1 || window.GetVisualDescendants().OfType