(key: string): T | null {
+ try {
+ const raw = sessionStorage.getItem(key);
+ if (!raw) return null;
+ return JSON.parse(raw) as T;
+ } catch {
+ return null;
+ }
+}
+
+export function writeSessionListCache(key: string, value: unknown): void {
+ try {
+ sessionStorage.setItem(key, JSON.stringify(value));
+ } catch {
+ /* private mode / quota */
+ }
+}
+
+export function clearSessionListCache(key: string): void {
+ try {
+ sessionStorage.removeItem(key);
+ } catch {
+ /* ignore */
+ }
+}
diff --git a/gui/src/styles.css b/gui/src/styles.css
index 29a04a56d..cce56b98c 100644
--- a/gui/src/styles.css
+++ b/gui/src/styles.css
@@ -825,9 +825,32 @@ dialog.modal-overlay::backdrop {
.quota-reset-time { font-size: var(--text-caption); color: var(--muted); white-space: nowrap; }
.quota-reset-time { font-family: var(--font-code); }
.bar { height: 5px; background: var(--raised); border-radius: var(--radius-2xs); overflow: hidden; min-width: 0; }
-.bar-fill { height: 100%; border-radius: var(--radius-2xs); transition: width var(--motion-normal); }
+.bar-fill {
+ height: 100%;
+ width: 100%;
+ border-radius: var(--radius-2xs);
+ transform: scaleX(var(--bar-scale, 0));
+ transform-origin: left center;
+ transition: transform var(--motion-normal);
+}
.bar-green { background: var(--green); }
.bar-amber { background: linear-gradient(90deg, var(--green), var(--amber)); }
+.quota-row--skeleton { min-height: 18px; }
+.quota-skel {
+ display: inline-block;
+ min-height: 8px;
+ border-radius: var(--radius-2xs);
+ background: color-mix(in srgb, var(--muted) 22%, transparent);
+}
+.quota-skel--label { width: 34px; }
+.quota-skel--reset { width: 34px; }
+.quota-skel--day { width: 36px; }
+.quota-skel--time { width: 38px; }
+.quota-skel--bar { min-height: 5px; width: 100%; }
+.quota-skel--val { width: 30px; }
+.openai-account-mode-banner__badge-slot--pending {
+ visibility: hidden;
+}
.toggle { width: var(--toggle-w); height: var(--toggle-h); border-radius: var(--radius-pill); background: var(--toggle-off-bg); border: 1px solid var(--border); cursor: pointer; position: relative; transition: background var(--motion-normal), border-color var(--motion-normal); flex-shrink: 0; }
.toggle.on { background: var(--toggle-on-bg); border-color: var(--toggle-on-bg); }
.toggle-knob { width: 16px; height: 16px; background: light-dark(#ffffff, #ececec); border-radius: var(--radius-round); position: absolute; top: 50%; left: 1px; transform: translateY(-50%); transition: left var(--motion-normal), background var(--motion-normal); }
diff --git a/gui/src/ui.tsx b/gui/src/ui.tsx
index 9477ba79a..e03d4b3b8 100644
--- a/gui/src/ui.tsx
+++ b/gui/src/ui.tsx
@@ -25,7 +25,7 @@ export function Notice({ tone, children }: { tone: "ok" | "err"; children: React
export interface SelectOption { value: string; label: React.ReactNode }
-export function Select({ value, options, onChange, disabled, label, style, align, placement, dropdownStyle, portal = true }: {
+export function Select({ value, options, onChange, disabled, label, style, align, placement, dropdownStyle, portal = true, id }: {
value: string;
options: SelectOption[];
onChange: (value: string) => void;
@@ -37,6 +37,8 @@ export function Select({ value, options, onChange, disabled, label, style, align
dropdownStyle?: CSSProperties;
/** When true (default), menu is portaled and flips above the trigger if it would leave the viewport. */
portal?: boolean;
+ /** Optional id on the trigger button (tests / labels target `#codex-pool-strategy`). */
+ id?: string;
}) {
const listboxId = useId();
const [open, setOpen] = useState(false);
@@ -204,6 +206,7 @@ export function Select({ value, options, onChange, disabled, label, style, align