Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
51969e7
feat(gui): isolate pool-workspace follow-up on fresh dev
agentHits Sep 17, 2026
833518d
fix(review): address CodeRabbit findings on pool-workspace PR
agentHits Sep 18, 2026
113c78e
fix(gui): seed missing vi locale keys, add pool workspace screenshot
agentHits Sep 19, 2026
5ff7cb6
fix(gui): repair regressions blocking PR checks
agentHits Sep 20, 2026
ee7e5c9
feat(gui): pool workspace follow-up plus reset-first backend
agentHits Sep 21, 2026
c27bad8
feat(gui): redesign provider accounts workspace
agentHits Sep 18, 2026
b975059
fix(gui): restore provider accounts regressions blocking PR checks
agentHits Sep 18, 2026
0b9d833
fix(gui): clear React Doctor blocking errors
agentHits Sep 20, 2026
1931be5
fix: repair layout map union and css imports after rebase
agentHits Sep 20, 2026
dcd42f0
fix(gui): clear react-doctor changed-scope warnings, refresh PR scree…
agentHits Sep 21, 2026
ce440ba
feat(gui): unify provider workspace from three PR branches
agentHits Sep 21, 2026
40f8c32
fix: keep dev 5h-fallback, repair modal and panel after unify
agentHits Sep 21, 2026
27169f1
fix(gui): address review findings on unified provider workspace
agentHits Sep 21, 2026
c30b8cd
fix(gui): register gui-account-quota-analysis in test-layout explicit…
agentHits Sep 23, 2026
5e9ec14
fix(gui): complete effectiveAccountFilter memo broken at PR head
lidge-jun Sep 24, 2026
d237f63
fix(gui): drop duplicated English keys overriding Vietnamese in vi lo…
agentHits Sep 24, 2026
deded85
fix(gui): address CodeRabbit findings - inert strategy state, grant b…
agentHits Sep 25, 2026
c95d602
fix(gui): French elision, Vietnamese pool translations, theme-aware p…
agentHits Sep 25, 2026
e6a93fc
fix(gui): drop duplicate sidebar locale keys, remove dead local colla…
agentHits Sep 26, 2026
d4112b3
fix: stale pool-scope fallback, quota-only freshness, removal dialog …
agentHits Sep 26, 2026
2ba8d36
Merge upstream/dev into agentHits/provider-workspace
agentHits Oct 12, 2026
c82dded
fix(gui): a superseded quota refresh joins only a newer successful ou…
agentHits Oct 12, 2026
ca4d2c5
fix(gui): show a saved-but-not-live pool strategy as such, and review…
agentHits Oct 12, 2026
b23cf5d
fix(gui): restore the accounts toolbar and card-grid styles
agentHits Oct 12, 2026
66f3514
Merge upstream/dev (8f10f238e) into agentHits/provider-workspace
agentHits Oct 12, 2026
ec229db
fix(gui): clear React Doctor findings in the accounts workspace
agentHits Oct 12, 2026
2db9838
Merge upstream/dev (bf57900f5) into agentHits/provider-workspace
agentHits Oct 12, 2026
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
Binary file added gui/public/provider-icons/cockpit-tools.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,10 @@ export default function App() {
navigateToPage(group.pages[0]);
setNavOpen(false);
}}
title={t(tkey)}
aria-label={t(tkey)}
aria-current={active ? "page" : undefined}>
<Icon /> {t(tkey)}
<Icon /> <span className="nav-item-text">{t(tkey)}</span>
</button>
</div>
);
Expand Down
123 changes: 123 additions & 0 deletions gui/src/components/AccountAuthChoiceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useEffect, useRef } from "react";
import { IconX } from "../icons";
import { useT } from "../i18n/shared";
import { ProviderIcon } from "./provider-workspace/ProviderRail";
import CockpitToolsCard from "./provider-workspace/CockpitToolsCard";

export interface AccountAuthChoiceModalProps {
provider: string;
providerLabel: string;
apiBase: string;
isOpen: boolean;
isBusy?: boolean;
onClose: () => void;
onContinueOAuth: () => void;
onImportSuccess?: () => void;
}

export default function AccountAuthChoiceModal({
provider,
providerLabel,
apiBase,
isOpen,
isBusy = false,
onClose,
onContinueOAuth,
onImportSuccess,
}: AccountAuthChoiceModalProps) {
const t = useT();
const dialog = useRef<HTMLDivElement>(null);
const primary = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!isOpen) return;
const previous = document.activeElement as HTMLElement | null;
primary.current?.focus();
// Deferred: React is still removing the dialog during cleanup and resets focus to the
// body afterwards, which would discard a synchronous restoration.
return () => { requestAnimationFrame(() => { if (previous?.isConnected && typeof previous.focus === "function") previous.focus(); }); };
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);

if (!isOpen) return null;

return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="auth-choice-modal-title"
className="modal-overlay"
onClick={onClose}
onKeyDown={event => {
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); onClose(); }
if (event.key !== "Tab") return;
const focusables = [...(dialog.current?.querySelectorAll<HTMLElement>("button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])") ?? [])].filter(el => el.offsetParent !== null);
const buttons = focusables as HTMLButtonElement[];
const first = buttons?.[0], last = buttons?.[buttons.length - 1];
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
}}
>
<div className="modal-card auth-choice-modal" ref={dialog} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div>
<h3 id="auth-choice-modal-title">
{t("pws.authChoiceModalTitle", { provider: providerLabel })}
</h3>
<p className="muted text-label" style={{ marginTop: 2 }}>
{t("pws.authChoiceModalSubtitle")}
</p>
</div>
<button
type="button"
className="btn btn-ghost btn-icon"
aria-label={t("common.close")}
onClick={onClose}
>
<IconX />
</button>
</div>

<div className="auth-choice-body">
{/* Method 1: Browser OAuth */}
<div className="auth-choice-card">
<div className="auth-choice-card-info">
<ProviderIcon name={provider} cls="provider-icon provider-icon-sm" />
<div className="auth-choice-card-text">
<span className="auth-choice-card-title">{t("pws.antigravityOauthTitle")}</span>
<span className="auth-choice-card-desc">{t("pws.antigravityOauthDesc")}</span>
</div>
</div>
<button
type="button"
className="btn btn-primary btn-sm"
ref={primary}
disabled={isBusy}
onClick={() => {
onClose();
onContinueOAuth();
}}
>
{isBusy ? t("prov.waitingBrowser") : t("pws.antigravityOauthAction")}
</button>
</div>

{/* Method 2: Cockpit Tools Card */}
<CockpitToolsCard
apiBase={apiBase}
onImportSuccess={onImportSuccess}
/>
</div>
</div>
</div>
);
}
22 changes: 18 additions & 4 deletions gui/src/components/AccountPoolStrategyControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface AccountPoolStrategyControlsProps {
strategy: AccountPoolStrategy;
codex?: boolean;
threshold?: number;
allowResetFirst?: boolean;
compact?: boolean;
stickyDraft: string;
disabled?: boolean;
strategySelectId?: string;
Expand All @@ -47,6 +49,8 @@ export default function AccountPoolStrategyControls({
strategy,
codex = false,
threshold,
allowResetFirst = false,
compact = false,
stickyDraft,
disabled = false,
strategySelectId = "account-pool-strategy",
Expand All @@ -56,7 +60,9 @@ export default function AccountPoolStrategyControls({
onStickyCommit,
}: AccountPoolStrategyControlsProps) {
const t = useT();
const strategyOptions = ACCOUNT_POOL_STRATEGIES.filter(value => codex || value !== "reset-first").map((value) => ({
const strategyOptions = ACCOUNT_POOL_STRATEGIES.filter(value => (
codex || allowResetFirst || value !== "reset-first" || strategy === value
)).map((value) => ({
value,
label: t(STRATEGY_LABEL_KEYS[value]),
}));
Expand Down Expand Up @@ -92,9 +98,17 @@ export default function AccountPoolStrategyControls({
<div className="setting-row">
<div className="setting-label">
<span className="title" id={`${strategySelectId}-label`}>{t("accountPool.strategy")}</span>
<span className="desc">{t("accountPool.strategyDesc")}</span>
<span className="desc">{t(STRATEGY_HINT_KEYS[strategy])}</span>
<span className="desc">{t("accountPool.unboundDefinition")}</span>
{compact ? (
<span className="desc">
{strategy === "reset-first" ? t("genericPool.visualResetFirst") : t(STRATEGY_HINT_KEYS[strategy])}
</span>
) : (
<>
<span className="desc">{t("accountPool.strategyDesc")}</span>
<span className="desc">{t(STRATEGY_HINT_KEYS[strategy])}</span>
<span className="desc">{t("accountPool.unboundDefinition")}</span>
</>
)}
</div>
<div className="setting-controls">
<Select
Expand Down
185 changes: 185 additions & 0 deletions gui/src/components/AccountPoolStrategyPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { useMemo, useState, type CSSProperties } from "react";
import { useT } from "../i18n/shared";
import type { AccountPoolStrategy } from "../account-pool-strategy";
import { barWidth, isQuotaExhausted, quotaBarTone } from "./QuotaBars";

type PoolKind = "generic" | "anthropic" | "codex";

function barStyle(percent: number): CSSProperties {
return { ["--bar-scale" as string]: String(barWidth(percent) / 100) };
}

/**
* Compact live diagram for the account-pool card. Generic OAuth keeps round-robin
* and fill-first stored until pool.kernel is on, so the picture shows the live
* Quota path and labels the selected strategy as saved when it is inert.
*/
export default function AccountPoolStrategyPreview({
strategy,
threshold,
kind,
enabled,
}: {
strategy: AccountPoolStrategy;
threshold: number;
kind: PoolKind;
enabled: boolean;
}) {
const t = useT();
const [usage, setUsage] = useState(90);
const storedOnly = kind === "generic" && strategy !== "quota" && strategy !== "reset-first";
const liveStrategy: AccountPoolStrategy = storedOnly ? "quota" : strategy;
const switchAt = useMemo(() => {
if (!enabled) return null;
if (liveStrategy === "fill-first") {
return threshold > 0 ? threshold : null;
}
if (liveStrategy === "round-robin" || liveStrategy === "reset-first") return null;
if (kind === "generic") return 100;
return threshold > 0 ? threshold : 100;
}, [enabled, kind, liveStrategy, threshold]);
const switches = switchAt !== null && usage >= switchAt;
const showTurns = liveStrategy === "round-robin" && !storedOnly;
const accountA = t("genericPool.visualAccountA");
const accountB = t("genericPool.visualAccountB");
const liveCaption = liveStrategy === "fill-first"
? t("genericPool.visualFillFirst", { threshold })
: liveStrategy === "round-robin"
? t("genericPool.visualRoundRobin")
: liveStrategy === "reset-first"
? t("genericPool.visualResetFirst")
: t("genericPool.visualQuota");

if (kind === "generic") {
return (
<div className="account-pool-preview" style={{ marginTop: "8px" }}>
<div className="account-pool-minimal-info" style={{
padding: "10px 14px",
borderRadius: "8px",
background: "var(--raised, #2a2a2a)",
border: "1px solid var(--border-soft, #383838)",
display: "flex",
flexDirection: "column",
gap: "6px",
fontSize: "13px",
}}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<strong style={{ color: "var(--text)" }}>
{strategy === "reset-first" && "📅 " + t("accountPool.strategyResetFirst")}
{strategy === "quota" && "⚡ " + t("accountPool.strategyQuota")}
{strategy === "round-robin" && "🔄 " + t("accountPool.strategyRoundRobin")}
{strategy === "fill-first" && "🎯 " + t("accountPool.strategyFillFirst")}
</strong>
{storedOnly && (
<span className="account-pool-preview__tag account-pool-preview__tag--stored">
{t("genericPool.visualStored")}
</span>
)}
</div>
<div className="card-sub" style={{ margin: 0, lineHeight: 1.4 }}>
{liveCaption}
</div>
<div className="card-sub" style={{ margin: 0, fontSize: "12px", color: "var(--muted)" }}>
{t("genericPool.visual429")}
</div>
</div>
</div>
);
}

return (
<div className="account-pool-preview">
<div className="account-pool-preview__head">
<span className="account-pool-preview__title">{t("genericPool.visualTitle")}</span>
{storedOnly && (
<span className="account-pool-preview__tag account-pool-preview__tag--stored">
{t("genericPool.visualStored")}
</span>
)}
</div>
<figure className="account-pool-preview__figure">
{showTurns ? (
<div className="account-pool-preview__turns" aria-hidden="true">
{[accountA, accountB, accountA].map((account, index) => (
<span key={`${account}-${index}`} className="account-pool-preview__turn is-on">
{t("genericPool.visualTurn", { n: index + 1, account })}
</span>
))}
</div>
) : (
<div className="quota-stacked" aria-hidden="true">
<PreviewAccount
name={accountA}
percent={usage}
threshold={switchAt ?? 100}
tag={switches ? null : t("genericPool.visualLive")}
/>
<PreviewAccount
name={accountB}
percent={12}
threshold={switchAt ?? 100}
tag={switches ? t("genericPool.visualLive") : null}
/>
</div>
)}
{!showTurns && (
<label className="account-pool-preview__slider">
<span className="field-label">{t("genericPool.visualUsage")}</span>
{switchAt !== null && (
<span className="card-sub">{t("genericPool.visualSwitchAt", { pct: switchAt })}</span>
)}
<input
type="range"
min={0}
max={100}
step={1}
value={usage}
aria-label={t("genericPool.visualUsageAria")}
onChange={(event) => setUsage(Number(event.target.value))}
/>
</label>
)}
{!showTurns && (
<figcaption className="account-pool-preview__caption">
{switches ? t("genericPool.visualSwitch") : t("genericPool.visualStay")}
</figcaption>
)}
</figure>
<div className="card-sub">{liveCaption}</div>
{storedOnly && <div className="card-sub">{t("genericPool.visualKernelGap")}</div>}
<div className="card-sub">{t("genericPool.visual429")}</div>
</div>
);
}

function PreviewAccount({
name,
percent,
threshold,
tag,
}: {
name: string;
percent: number;
threshold: number;
tag: string | null;
}) {
const t = useT();
const exhausted = isQuotaExhausted(percent);
const color = quotaBarTone(percent, threshold);
return (
<div className={`quota-stacked-row${exhausted ? " quota-stacked-row--exhausted" : ""}`}>
<div className="account-pool-preview__row-head">
<span className="account-pool-preview__name">{name}</span>
{tag && <span className="account-pool-preview__tag">{tag}</span>}
</div>
<div className="quota-stacked-bar-row">
<div className="bar quota-stacked-bar">
<div className={`bar-fill ${color}`} style={barStyle(percent)} />
</div>
<span className={`quota-stacked-used${color === "bar-warn" ? " quota-stacked-used--warn" : ""}`}>
{t("quota.usedPercent", { pct: Math.round(percent) })}
</span>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions gui/src/components/AntigravityChoiceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useT } from "../i18n/shared";
import AccountAuthChoiceModal, { type AccountAuthChoiceModalProps } from "./AccountAuthChoiceModal";

export type AntigravityChoiceModalProps = Omit<AccountAuthChoiceModalProps, "provider" | "providerLabel"> & {
provider?: string;
providerLabel?: string;
};

export default function AntigravityChoiceModal(props: AntigravityChoiceModalProps) {
const t = useT();
return (
<AccountAuthChoiceModal
provider={props.provider ?? "google-antigravity"}
providerLabel={props.providerLabel ?? t("pws.providerLabelAntigravity")}
{...props}
/>
);
}
Loading
Loading