Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Focused dialogs may handle add-account, explicit confirmation, progress/results,
or recovery; they do not create a second navigation system.

The toolbar contains the current Codex account, Refresh, Wake all, Add account,
and a lightweight Settings button. It is a command bar, not a dashboard header
and a compact language menu. It is a command bar, not a dashboard header
or a custom window chrome. Its status dot always remains visible; only a long
account label may truncate. The status is also written in text so ready, unknown,
signed-out, setup, and recovery states do not rely on color.
Expand All @@ -29,7 +29,7 @@ An account card should show only the state needed for a decision:

- label and useful identity;
- active, ready, needs-login, unsupported, or busy state;
- five-hour and weekly quota with reset timing when available;
- the Codex quota windows actually supplied for that plan, with reset timing when available;
- reset-credit count and nearest expiry when available;
- direct actions such as Switch, Wake, refresh, reset, or remove when eligible.

Expand Down Expand Up @@ -88,8 +88,14 @@ explicit reset preserves GSwitch's damaged file without touching Codex.
current identity stays protected. Operation-lock failures remain visible in
the confirmation with a retry instruction.
- Quota refreshes share a serialized request path across startup and manual
refresh; failed cards identify stale data or show quota as unavailable.
Batch feedback describes quota results only.
refresh. A failed card keeps a visible, compact warning icon beside quota;
its hover/focus explanation gives the safe reason and last successful update.
Old percentages say "Last" and use muted meters; an unavailable result stays
unknown. Actions stay in the account menu, not in the warning tooltip. Batch
feedback describes quota results only.
- A saved email can be copied from its card menu. Offer sign-in again only after
an authentication failure. The focused login dialog explains which saved
account will be updated; a mismatched login changes no account.
- Keep card content compact. Truncated account and workspace names expose their
full value on hover; do not reserve empty vertical space for the old heading
reminder.
Expand All @@ -108,8 +114,8 @@ filesystem, protocol, or token details do not belong in the primary UI.

The application ships English and Simplified Chinese in one frontend resource.
On first launch it maps a compatible system/WebView locale to one of those
languages and otherwise falls back to English. Settings offers only System /
Automatic, English, and Simplified Chinese; a manual choice applies immediately
languages and otherwise falls back to English. The toolbar language menu offers
System / Automatic, English, and Simplified Chinese; a manual choice applies immediately
and wins over automatic detection. The only persisted WebView preference is that
language choice. Dates, reset/expiry timing, numbers, and percentages use the
selected locale's platform formatters.
Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ and expected prior state.

Before replacing live credentials:

1. the effective store must be confirmed as file-backed and unmanaged;
1. the live profile must explicitly use the file store, and the official App Server in a short-lived GSwitch-owned profile must confirm that machine policy does not override it; do not initialize App Server against the live profile for this check;
2. no external Codex runtime may be active or uninspectable, and that check must
happen before target-network validation;
3. the current live identity must be saved or the live profile must be empty;
Expand Down
21 changes: 18 additions & 3 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ the user's live Codex identity.

1. Create an isolated GSwitch-owned `CODEX_HOME`.
2. Ask the official Codex App Server to start ChatGPT login.
3. Open or copy the returned HTTPS authorization URL.
3. Show the returned HTTPS authorization URL for copying or an explicit
**Open browser** action. Do not open it automatically. Use Codex's local
success page so completing login does not launch the ChatGPT desktop app.
4. Wait for the matching completion event, read the complete resulting
credential document, and use its fresh access-token snapshot for the
read-only account metadata check. Do not request a proactive refresh.
Expand All @@ -23,6 +25,11 @@ the user's live Codex identity.

Cancellation and timeout end the isolated login. OAuth is the default login
experience; GSwitch does not implement a parallel OAuth protocol.
For **Sign in again** on a saved account, retain the selected account ID and
compare the newly verified user and workspace identity, plus an available
email, before replacing that account's saved credential. A different login,
removed account, or changed identity leaves the saved account and live Codex
credential unchanged. The WebView receives only a safe failure category.
The App Server is stopped before the isolated profile is removed, including
after cancellation or validation failure; cleanup failure is reported.

Expand Down Expand Up @@ -196,7 +203,9 @@ when it is not. GSwitch sends no App Server request and writes no credential
for a successful ordinary read. It normalizes the provider's primary,
secondary, and additional buckets into five-hour, weekly, and other windows by
the durations supplied by the provider; missing or malformed values remain
unknown.
unknown. The card renders whichever Codex windows the provider actually supplies,
including a five-week Free-plan window, with its reset time. It does not
invent a five-hour or weekly window for a plan that lacks one.

The supported minimum is Codex 0.144.5. GSwitch sends its rate-limit request
with a null parameter payload for that version and retries once with an empty
Expand All @@ -217,7 +226,9 @@ stale ChatGPT accounts in the background. Startup and manual refreshes share one
serial request queue, and requests for the same account join the in-flight
request. One failed account does not stop the remaining queue. Its card keeps
the last result marked stale, or shows quota as unavailable when no snapshot
exists; the batch notice reports only failed quota refreshes. Adding, importing,
exists. A compact warning on that card explains the safe failure category and
last successful update; old percentages are explicitly labelled as the last
result. The batch notice reports only failed quota refreshes. Adding, importing,
or saving an account follows the same refresh path. When a running Codex instance is
identified as using the account, GSwitch rereads the live file-backed
credential immediately before the request and retries once only when the same
Expand Down Expand Up @@ -299,6 +310,10 @@ silently relabel another account. Wake is user-triggered; there is no cron,
background schedule, automatic rotation, history dashboard, or job-management
surface.

When the freshly read quota specifically shows a zero five-hour or weekly
balance in an active window, name that window in the result. Otherwise say only
that Wake has no available quota; do not infer which limit was exhausted.

## Recovery

Interrupted switch and reset-credit actions retain non-secret durable intent
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "gswitch",
"private": true,
"version": "1.0.7",
"version": "1.0.8",
"type": "module",
"packageManager": "pnpm@10.17.1",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gswitch"
version = "1.0.7"
version = "1.0.8"
description = "A focused local Codex account switcher"
authors = ["Ginbing"]
edition = "2021"
Expand Down
22 changes: 15 additions & 7 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use crate::{
codex, intake, migration, quota, switching,
types::{
AccountView, AppSnapshot, ExportResult, ImportResult, LiveAccountView, MigrationPreview,
OAuthLoginStart, OAuthLoginStatus, QuotaView, ResetCreditOutcome, RuntimeInfo,
StorageStatus, SwitchFailure, SwitchFailureCode, SwitchOutcome, UpdateDelivery,
WakeOperationView, WakeStart,
OAuthLoginStart, OAuthLoginStatus, QuotaRefreshFailure, QuotaRefreshFailureCode, QuotaView,
ResetCreditOutcome, RuntimeInfo, StorageStatus, SwitchFailure, SwitchFailureCode,
SwitchOutcome, UpdateDelivery, WakeOperationView, WakeStart,
},
wake,
};
Expand Down Expand Up @@ -85,9 +85,12 @@ pub async fn recover_pending_credentials(state: State<'_, AppState>) -> Result<u
}

#[tauri::command]
pub async fn start_oauth_login(state: State<'_, AppState>) -> Result<OAuthLoginStart, String> {
pub async fn start_oauth_login(
state: State<'_, AppState>,
target_id: Option<String>,
) -> Result<OAuthLoginStart, String> {
let state = state.inner().clone();
run_blocking(move || intake::start_oauth(state)).await
run_blocking(move || intake::start_oauth(state, target_id)).await
}

#[tauri::command]
Expand Down Expand Up @@ -278,9 +281,14 @@ pub async fn get_account_quota(
pub async fn refresh_account_quota(
state: State<'_, AppState>,
id: String,
) -> Result<QuotaView, String> {
) -> Result<QuotaView, QuotaRefreshFailure> {
let state = state.inner().clone();
run_blocking(move || quota::refresh_quota(&state, &id)).await
tauri::async_runtime::spawn_blocking(move || quota::refresh_quota(&state, &id))
.await
.map_err(|_| QuotaRefreshFailure {
code: QuotaRefreshFailureCode::Unavailable,
})?
.map_err(|error| quota::refresh_failure(&error))
}

#[tauri::command]
Expand Down
Loading
Loading