Skip to content
Open
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: 15 additions & 3 deletions docs/i18n-inventory.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# i18n inventory (egdev6/quorb#66)

Generated by `node scripts/i18n-inventory.mjs` from the source trees `src/` = `0cda9fef36e559dcffcd3bf53611c07f89c49a2b` and `src-tauri/src/` = `fa2fd75f1124453a881a835169cde8f4ffb08839`.
Generated by `node scripts/i18n-inventory.mjs` from the source trees `src/` = `2a5034e8b205b956e2024909c4dbb06e9732721d` and `src-tauri/src/` = `68f24ee0259edabaea42a3ec732af7b8d40eaca6`.
Any commit whose `git rev-parse <commit>:src` and `<commit>:src-tauri/src` match has these exact texts.
Do not edit by hand: regenerate, or verify with `node scripts/i18n-inventory.mjs --check`.

Expand All @@ -18,9 +18,9 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a

| Area | Distinct texts | Sites |
|---|---:|---:|
| React (`src/`) | 958 | 1069 |
| React (`src/`) | 969 | 1081 |
| Rust (`src-tauri/src/`) | 508 | 625 |
| Total | 1428 | 1694 |
| Total | 1439 | 1706 |

## src-tauri/src/channel.rs

Expand Down Expand Up @@ -828,8 +828,10 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Te hemos enviado un enlace. Cópialo (sin abrirlo) y pégalo aquí.
- Todo
- USD al mes
- Ubuntu-22.04
- Ver respuestas de /v1/models y /v1/usage
- Volver
- WSL
- Wayland
- \\.claude\\.credentials.json
- \\\\wsl.localhost\\Ubuntu\\home\\
Expand Down Expand Up @@ -967,6 +969,7 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Balance and spend per key
- Bottom
- Budget
- CLI session host
- Change
- ChatGPT and Codex
- Check
Expand Down Expand Up @@ -1015,6 +1018,7 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Delete key
- Desktop session: {session}
- Disabled
- Distro where the CLIs and their sessions live (e.g. Ubuntu-22.04).
- Done
- Drag: snaps to the nearest edge
- Each account keeps its session in its own folder, so it does not touch the main one: the login opens with
Expand All @@ -1041,6 +1045,7 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Hide on its own
- History
- History range
- Host uses the Windows credentials; WSL uses the distro ones.
- How to connect the account
- How to connect this account
- In addition to the on-screen effect.
Expand Down Expand Up @@ -1229,12 +1234,14 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Use {color}
- Video or animated image
- Videos only.
- WSL distro
- Week
- Weekly credit usage of your subscription in the Grok CLI. Quorb reads the session stored by
- What gets checked and how each provider connects.
- What happens when a limit is exceeded and when it resets.
- When to alert
- Window positioning
- Windows
- With sound
- Without it only the plan quota is shown; the balance is optional and a failure there does not degrade it.
- Works
Expand Down Expand Up @@ -1421,6 +1428,8 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Derecha
- Desactivado
- Detalle de {name}
- Distro WSL
- Distro donde viven los CLIs y sus sesiones (p. ej. Ubuntu-22.04).
- Efecto
- Efecto propio
- Efecto propio (vídeo o imagen animada)
Expand Down Expand Up @@ -1463,6 +1472,7 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Hay una versión nueva: {version}.
- Historial
- Historial de consumo
- Host usa las credenciales de Windows; WSL, las de la distro.
- Icono de bandeja
- Idioma
- Inicia sesión
Expand Down Expand Up @@ -1565,6 +1575,7 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Secciones de ajustes
- Semana
- Sesión de escritorio: {session}
- Sesión de los CLIs
- Siempre encima
- Sin atajo
- Sin configurar
Expand Down Expand Up @@ -1618,6 +1629,7 @@ tuples (e.g. the `"req"` unit in `providers/nan.rs`) are only listed when they a
- Vista previa a {size} px
- Volver a abrir el login
- Vídeo o imagen animada
- Windows
- apuntando a ella. Si la comprobación pide renovar la sesión, «{reopen}» abre de nuevo el CLI.
- caché
- cada {mins} min
Expand Down
6 changes: 1 addition & 5 deletions src-tauri/src/providers/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,7 @@ pub async fn fetch(
// (`wsl cat`) en vez del disco del host. Sin resolvedor, el
// comportamiento de siempre (disco local).
let default_path = match resolver {
Some(r) => r.default_path(
super::provider_session_relative("claude")
.expect("proveedor CLI")
.as_path(),
),
Some(r) => r.default_path(super::session_relative(super::CliSessionKind::Claude).as_path()),
None => home().join(".claude").join(".credentials.json"),
};
let creds = match resolver {
Expand Down
7 changes: 2 additions & 5 deletions src-tauri/src/providers/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,8 @@ pub async fn fetch(
// comportamiento de siempre (disco local).
let auth_json: Value = match resolver {
Some(r) => {
let default = r.default_path(
super::provider_session_relative("codex")
.expect("proveedor CLI")
.as_path(),
);
let default =
r.default_path(super::session_relative(super::CliSessionKind::Codex).as_path());
let path = match r.resolve(cfg.credentials_path.as_deref(), &default) {
Ok(p) => p,
Err(e) => return report.fail(Status::Error, e.to_string()),
Expand Down
7 changes: 2 additions & 5 deletions src-tauri/src/providers/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,8 @@ pub async fn fetch(
// comportamiento de siempre (disco local).
let creds: Value = match resolver {
Some(r) => {
let default = r.default_path(
super::provider_session_relative("gemini")
.expect("proveedor CLI")
.as_path(),
);
let default =
r.default_path(super::session_relative(super::CliSessionKind::Gemini).as_path());
let path = match r.resolve(cfg.credentials_path.as_deref(), &default) {
Ok(p) => p,
Err(e) => return report.fail(Status::Error, e.to_string()),
Expand Down
7 changes: 2 additions & 5 deletions src-tauri/src/providers/grok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,8 @@ pub async fn fetch(
// comportamiento de siempre (disco local).
let auth_json: Value = match resolver {
Some(r) => {
let default = r.default_path(
super::provider_session_relative("grok")
.expect("proveedor CLI")
.as_path(),
);
let default =
r.default_path(super::session_relative(super::CliSessionKind::Grok).as_path());
let path = match r.resolve(cfg.credentials_path.as_deref(), &default) {
Ok(p) => p,
Err(e) => return report.fail(Status::Error, e.to_string()),
Expand Down
116 changes: 96 additions & 20 deletions src-tauri/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,18 +248,27 @@ pub fn session_file(cfg: &AppConfig, id: &str, home: &Path) -> Option<PathBuf> {
}

/// Consulta una cuenta adicional con la config de su proveedor adaptada a ella.
/// Proveedor con sesión CLI, para `session_relative` (una sola fuente,
/// sin `expect` en runtime; revisión del mantenedor, 2026-11-06).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CliSessionKind {
Claude,
Codex,
Gemini,
Grok,
}

/// Ruta RELATIVA de la sesión de cada proveedor CLI (una sola fuente para
/// los cuatro `fetch` y `wsl_session_stamp`; revisión del mantenedor,
/// 2026-11-06). `None` para proveedores sin login por CLI.
pub(crate) fn provider_session_relative(provider: &str) -> Option<PathBuf> {
let parts: &[&str] = match provider {
"claude" => &[".claude", ".credentials.json"],
"codex" => &[".codex", "auth.json"],
"gemini" => &[".gemini", "oauth_creds.json"],
"grok" => &[".grok", "auth.json"],
_ => return None,
/// los cuatro `fetch` y `wsl_session_stamp`). Match exhaustivo sobre el
/// enum: no hay `expect` posible.
pub(crate) fn session_relative(kind: CliSessionKind) -> PathBuf {
let parts: &[&str] = match kind {
CliSessionKind::Claude => &[".claude", ".credentials.json"],
CliSessionKind::Codex => &[".codex", "auth.json"],
CliSessionKind::Gemini => &[".gemini", "oauth_creds.json"],
CliSessionKind::Grok => &[".grok", "auth.json"],
};
Some(crate::wsl::guest_relative(parts))
crate::wsl::guest_relative(parts)
}

/// La ruta de sesión de una cuenta ADICIONAL en el entorno del resolvedor
Expand Down Expand Up @@ -560,26 +569,26 @@ fn host_session_stamp(cfg: &AppConfig) -> u64 {
/// mtime accesible (revisión PR #220).
async fn wsl_session_stamp(cfg: &AppConfig, r: &SessionResolver) -> u64 {
// La ruta relativa sale de la MISMA fuente que los fetch
// (`provider_session_relative`; revisión del mantenedor, 2026-11-06).
let main = |c: &SubscriptionConfig, provider: &str| {
let relative = provider_session_relative(provider).expect("proveedor CLI");
// (`session_relative` sobre el enum; revisión del mantenedor, 2026-11-06).
let main = |c: &SubscriptionConfig, kind: CliSessionKind| {
let relative = session_relative(kind);
r.resolve(
c.credentials_path.as_deref(),
&r.default_path(relative.as_path()),
)
.ok()
};
let mut paths: Vec<PathBuf> = Vec::new();
if let Some(p) = main(&cfg.claude, "claude") {
if let Some(p) = main(&cfg.claude, CliSessionKind::Claude) {
paths.push(p);
}
if let Some(p) = main(&cfg.codex, "codex") {
if let Some(p) = main(&cfg.codex, CliSessionKind::Codex) {
paths.push(p);
}
if let Some(p) = main(&cfg.gemini, "gemini") {
if let Some(p) = main(&cfg.gemini, CliSessionKind::Gemini) {
paths.push(p);
}
if let Some(p) = main(&cfg.grok, "grok") {
if let Some(p) = main(&cfg.grok, CliSessionKind::Grok) {
paths.push(p);
}
for a in cfg.accounts.iter().filter(|a| a.enabled) {
Expand All @@ -590,9 +599,13 @@ async fn wsl_session_stamp(cfg: &AppConfig, r: &SessionResolver) -> u64 {
paths.push(PathBuf::from(path));
}
}
// Lecturas en PARALELO (issue #243): el sello corre cada 15 s y no debe
// pagar un `wsl.exe cat` por proveedor en serie. La suma es conmutativa
// (wrapping_add), así que el orden de resultados no altera la huella.
let results = futures::future::join_all(paths.iter().map(|p| r.read_async(p))).await;
let mut acc = 0u64;
for p in paths {
if let Ok(contents) = r.read_async(&p).await {
for result in results {
if let Ok(contents) = result {
acc = acc.wrapping_add(content_hash(contents.as_bytes()));
}
}
Expand Down Expand Up @@ -625,7 +638,7 @@ pub(crate) fn net_error(e: reqwest::Error) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::wsl::{distro, DistroState, MockWslAdapter};
use crate::wsl::{distro, DistroState, MockWslAdapter, ShellStrategy};
use std::sync::Arc;

/// #91 (revisión PR #220): el sello en WSL observa los ficheros DENTRO de
Expand Down Expand Up @@ -731,6 +744,69 @@ mod tests {
/// reactor, que el tokio de CI no expone; la decisión de ruta está
/// aislada aquí y su consumidor en el sello se prueba con el `WslCall`
/// exacto.)

/// Regresión directa del `WslCall` desde `fetch_account` (issue #243,
/// revisión del mantenedor): el invitado debe recibir la ruta POSIX
/// EXACTA de la cuenta adicional. La sesión de Claude con token vencido
/// responde NeedsLogin SIN tocar la red, así que el test es
/// determinista: `expiresAt` en el pasado.
#[test]
fn fetch_account_sends_the_exact_guest_path_for_extra_accounts() {
let mock = Arc::new(MockWslAdapter::new());
mock.set_distributions(vec![distro("Ubuntu-22.04", DistroState::Running)]);
mock.set_shell("Ubuntu-22.04", ShellStrategy::BashLogin);
mock.set_home("Ubuntu-22.04", PathBuf::from("/home/user"));
let extra = PathBuf::from("/home/user/.quorb/accounts/claude-w1/.credentials.json");
mock.set_file(
"Ubuntu-22.04",
extra.clone(),
r#"{"claudeAiOauth":{"accessToken":"vencido","expiresAt":1000}}"#.to_string(),
);
let r = crate::wsl::SessionResolver::for_wsl(
mock.clone(),
"Ubuntu-22.04".into(),
PathBuf::from("/home/user"),
);
let cfg = AppConfig {
accounts: vec![Account {
id: "claude:w1".into(),
provider: "claude".into(),
enabled: true,
..Default::default()
}],
..Default::default()
};
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime de un hilo");
// El cliente se construye DENTRO del contexto del runtime (`enter`):
// el crate de tokio del harness no expone reactor fuera de él y
// `reqwest::Client::new` exige un Handle activo.
let _enter = rt.enter();
let http = reqwest::Client::new();
let report = rt
.block_on(tokio::time::timeout(
std::time::Duration::from_secs(10),
fetch_account(&cfg, &cfg.accounts[0], &http, Some(&r)),
))
.expect("no debe colgar");
let status = &report.status;
assert!(
matches!(status, crate::model::Status::NeedsLogin),
"token vencido: {status:?} {:?}",
report.message
);
assert!(
mock.calls().iter().any(|c| matches!(
c,
crate::wsl::WslCall::ReadFile(d, p)
if d == "Ubuntu-22.04" && p == &extra
)),
"el guest recibe la ruta POSIX exacta, llamadas: {:?}",
mock.calls()
);
}
/// La composición POSIX de la cuenta adicional: absoluta del guest se
/// usa tal cual; separadores del host o rutas relativas se rechazan con
/// mensaje accionable (revisión del mantenedor, 2026-11-04).
Expand Down
5 changes: 4 additions & 1 deletion src/Settings-providers-i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const enabledOnly = (...ids: string[]) => {
const on = (id: string) => ({ enabled: ids.includes(id), username: 'egdev6' });
return {
refreshMinutes: 5,
sessionEnv: 'host',
wslDistro: '',
claude: on('claude'),
codex: on('codex'),
gemini: on('gemini'),
Expand All @@ -63,12 +65,13 @@ const cfg = enabledOnly('claude', 'copilot');

const card = (locale: Locale, tab: 'claude' | 'copilot' = 'claude', drawerOpen = true) =>
renderToStaticMarkup(
<ProvidersCard locale={locale} cfg={cfg} secrets={{}} tab={tab} drawerOpen={drawerOpen} onSelectTab={noop} onRefreshMinutes={noop} />,
<ProvidersCard locale={locale} cfg={cfg} secrets={{}} tab={tab} drawerOpen={drawerOpen} onSelectTab={noop} onRefreshMinutes={noop} onSessionEnv={noop} onWslDistro={noop} />,
);

const descriptions: [es: string, en: string][] = [
['Pro / Max · sesión de 5 h, semanal y por modelo', 'Pro / Max · 5 h session, weekly and per-model'],
['Plus / Pro · sesión y semanal de Codex', 'Plus / Pro · Codex session and weekly'],
['Sesión de los CLIs', 'CLI session host'],
['Cuota diaria por modelo', 'Daily quota per model'],
['Uso del plan en el ciclo y on-demand', 'Plan usage in the cycle and on-demand'],
['SuperGrok · uso semanal de créditos del CLI', 'SuperGrok · weekly CLI credit usage'],
Expand Down
21 changes: 20 additions & 1 deletion src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1290,14 +1290,16 @@ function ProviderCard({ locale, tab: p, state: st, extraAccounts, open, onSelect

/** Tarjeta de la página «Proveedores»: intervalo de consulta y las dos listas
* (suscripciones y APIs). Extraída para poder renderizarla en pruebas de i18n. */
export function ProvidersCard({ locale, cfg, secrets, tab, drawerOpen, onSelectTab, onRefreshMinutes }: {
export function ProvidersCard({ locale, cfg, secrets, tab, drawerOpen, onSelectTab, onRefreshMinutes, onSessionEnv, onWslDistro }: {
locale: Locale;
cfg: AppConfig;
secrets: SecretStatus;
tab: ProviderTab;
drawerOpen: boolean;
onSelectTab: (id: ProviderTab) => void;
onRefreshMinutes: (minutes: number) => void;
onSessionEnv: (env: 'host' | 'wsl') => void;
onWslDistro: (distro: string) => void;
}) {
const providerCard = (p: (typeof PROVIDER_TABS)[number]) => (
<ProviderCard
Expand All @@ -1321,6 +1323,21 @@ export function ProvidersCard({ locale, cfg, secrets, tab, drawerOpen, onSelectT
<input type="number" min={1} max={120} value={cfg.refreshMinutes} onChange={(e) => onRefreshMinutes(Number(e.target.value) || 1)} />
{t(locale, 'settings.providers.refreshUnit')}
</label>
<label className="q-section__aside">
{t(locale, 'settings.providers.sessionEnv')}
<select value={cfg.sessionEnv} onChange={(e) => onSessionEnv(e.target.value as 'host' | 'wsl')}>
<option value="host">{t(locale, 'settings.providers.sessionHost')}</option>
<option value="wsl">WSL</option>
</select>
<small className="eg-field__hint">{t(locale, 'settings.providers.sessionEnvHint')}</small>
</label>
{cfg.sessionEnv === 'wsl' && (
<label className="q-section__aside">
{t(locale, 'settings.providers.wslDistro')}
<input type="text" value={cfg.wslDistro} placeholder="Ubuntu-22.04" onChange={(e) => onWslDistro(e.target.value)} />
<small className="eg-field__hint">{t(locale, 'settings.providers.wslDistroHint')}</small>
</label>
)}
</div>
<div className="eg-module-card__body">
<p className="eg-eyebrow">{t(locale, 'settings.providers.subscriptions')}</p>
Expand Down Expand Up @@ -2580,6 +2597,8 @@ export function Settings() {
drawerOpen={drawerOpen}
onSelectTab={selectTab}
onRefreshMinutes={(minutes) => set('refreshMinutes', minutes)}
onSessionEnv={(env) => set('sessionEnv', env)}
onWslDistro={(distro) => set('wslDistro', distro)}
/>
<div className="q-drawer-scrim" data-open={drawerOpen || undefined} onClick={closeDrawer} aria-hidden="true" />
<aside
Expand Down
Loading
Loading