Skip to content

Commit d0141e7

Browse files
committed
fix(ci): repair the prepush gate after the 260730 GUI stack
dev was red on both lint:gui (13 errors) and the suite (3 failures) before this commit. All four reproduce on a pristine origin/dev worktree, so none of them came from the blocker fixes stacked underneath; the 260730 GUI PRs landed them. Tests, where the assertion had gone stale rather than the code being wrong: - cli-help: ee3eaa2 added a real `repair` subcommand plus repairService(), so the expectation was the stale side. The full usage string is still pinned, so the `remove` alias this test exists for still fails if it disappears. - provider-workspace-auth: c4c98c0 renamed setToast to showActionFeedback. The replacement also requires the literal `true` second argument, so the error-tone contract is pinned slightly tighter than before. - windows-scheduler: the new UTF-16LE test hardcoded an absolute wscript path, but buildWindowsTaskXml resolves <Command> through windowsWscript(), which falls back to a bare "wscript.exe" wherever System32 does not exist. It could never have passed off Windows. Pinned <Command> the way the sibling describe block in the same file already does; the decoder itself is still exercised, including the sanity check that a UTF-8 mis-decode stays unhealthy. Lint. Five were false positives fixed in the allowlist rather than by inventing i18n keys for things that must not be translated: GiB is an IEC unit, and three curl samples resume after `${...}` interpolation as `\ -H ...`, which the bare `^-H` rule cannot see. Four were mechanically equivalent and got real fixes — a redundant tooltip coordinate reset, a redundant delete-confirm reset, a ref read during render that `loading` already encodes, and the Logs surface filter moved into its own module so the page exports components only. Three kept their behavior and got a narrow suppression instead, because "fix the lint" is not worth silently changing UX in code that landed hours ago: ProviderAuthPanel's quota reservation is a TIMED state machine whose four-second expiry stops a stalled enrichment from pinning skeleton rows forever; Models' default collapse stays an effect so cached groups collapse on first paint even when revalidation is slow or fails; CodexPoolStrategySetting's load() reaches an await before any setter, so it never synchronously cascades. Each comment states the contract it preserves. Gates now: typecheck, lint:gui, privacy:scan, doctor:gui:if-changed all clean, and the suite is 6074 pass / 0 fail across 437 files. The gui/ test suite has 14 pre-existing failures that are identical before and after this commit and are not part of the prepush set.
1 parent 0b86278 commit d0141e7

13 files changed

Lines changed: 62 additions & 26 deletions

gui/.eslint/i18n-allowlist.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const TECHNICAL_UNITS = new Set([
4646
"Mo",
4747
"Mi",
4848
"Fr",
49+
// IEC binary unit rendered next to a formatted number; a unit symbol, not UI prose.
50+
"GiB",
4951
]);
5052

5153
/** Non-UI technical strings (API paths, CSS, shell, headers, debug fields). */
@@ -87,6 +89,11 @@ export function isTechnicalLiteral(value: string): boolean {
8789
if (/^curl\b/i.test(trimmed)) return true;
8890
if (/^-H\b/.test(trimmed)) return true;
8991
if (/^-d\b/.test(trimmed)) return true;
92+
// Line-continuation fragment: a multi-line curl sample split by `${...}` interpolation
93+
// resumes with the escaped backslash and a newline before the next flag, so the bare
94+
// `^-H` rule above never sees it. Still a shell sample, still not translatable. The
95+
// leading run is one-or-more because the raw template text keeps the escape.
96+
if (/^\\+\s+-(?:H|d)\b/.test(trimmed)) return true;
9097
if (/^ocx\b/i.test(trimmed)) return true;
9198
if (/^codex\b/i.test(trimmed)) return true;
9299

gui/src/components/CodexPoolStrategySetting.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ export default function CodexPoolStrategySetting({
143143
// Standalone fallback when no shared controller observer is wired (keeps tests without observer green).
144144
useEffect(() => {
145145
if (subscribeLoadObserver) return;
146+
// Not a synchronous cascade: load() reaches `await fetch(...)` before it touches any
147+
// setter, so state never updates during this effect's own render pass.
148+
// eslint-disable-next-line react-hooks/set-state-in-effect
146149
void load();
147150
}, [load, subscribeLoadObserver]);
148151

gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,9 @@ export default function ApiKeysWorkspace({
9595
};
9696

9797
useEffect(() => {
98-
if (!confirmDelete) {
99-
setConfirmArmed(false);
100-
return;
101-
}
98+
// Every path that closes confirmation goes through clearDeleteConfirm(), which
99+
// already resets confirmArmed, so resetting again here was a redundant cascade.
100+
if (!confirmDelete) return;
102101
const timer = window.setTimeout(() => setConfirmArmed(true), 300);
103102
return () => window.clearTimeout(timer);
104103
}, [confirmDelete]);

gui/src/components/provider-workspace/ProviderAuthPanel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,14 @@ export default function ProviderAuthPanel({
6060

6161
// Soft &quota=1 enrichment lands after the local account list. Reserve stacked
6262
// bar height briefly so bars don't shove rows when WHAM returns.
63+
//
64+
// Deliberately a timed state machine, not a derived value: the reservation must EXPIRE
65+
// after QUOTA_ENRICH_RESERVE_MS so a stalled enrichment cannot leave skeleton rows up
66+
// forever. A plain `accounts.some(...)` boolean would drop that bound, so the rule is
67+
// suppressed here rather than refactored away.
6368
useEffect(() => {
6469
if (accounts.length === 0) {
70+
// eslint-disable-next-line react-hooks/set-state-in-effect
6571
setReserveQuotaSlots(false);
6672
return;
6773
}

gui/src/pages/Combos.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,10 @@ export default function Combos({ apiBase }: { apiBase: string }) {
235235
}
236236
};
237237

238-
// Cold start only — cached empty lists paint the workspace immediately.
239-
if (loading && !hasCacheRef.current) {
238+
// Cold start only — cached empty lists paint the workspace immediately. `loading`
239+
// already encodes that: it initializes to !seedCombos(cacheKey) and a refresh only
240+
// sets it when the cache is empty, so reading the ref during render was redundant.
241+
if (loading) {
240242
return (
241243
<div className="combos-workspace-shell">
242244
{status && (

gui/src/pages/Logs.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import Debug from "./Debug";
1313
import type { LogsTab } from "./logs-tab-keydown";
1414
import { logsTabKeyDown, readTabFromHash, selectLogsTab } from "./logs-tab-keydown";
1515
import { speedLabel } from "./logs-speed-label";
16+
import type { LogSurface, LogSurfaceFilter } from "./logs-surface-filter";
17+
import { logMatchesSurface } from "./logs-surface-filter";
1618

1719
function logsCacheKey(apiBase: string): string {
1820
return `ocx.logs.list.v1:${apiBase}`;
@@ -101,17 +103,6 @@ interface LogAttempt {
101103
displayMetrics?: LogDisplayMetrics;
102104
}
103105

104-
type LogSurface = "claude" | "claude-desktop" | "grok";
105-
type LogSurfaceFilter = "all" | "claude" | "codex" | "grok";
106-
107-
/** Match Usage surface buckets: Claude includes Desktop; Codex is untagged. */
108-
export function logMatchesSurface(log: { surface?: LogSurface }, filter: LogSurfaceFilter): boolean {
109-
if (filter === "all") return true;
110-
if (filter === "claude") return log.surface === "claude" || log.surface === "claude-desktop";
111-
if (filter === "grok") return log.surface === "grok";
112-
return log.surface === undefined;
113-
}
114-
115106
export interface LogEntry {
116107
requestId?: string;
117108
timestamp: number;

gui/src/pages/Models.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,16 @@ export default function Models({ apiBase }: { apiBase: string }) {
271271
[models, providers],
272272
);
273273

274+
// One-shot default collapse. It stays an effect on `groups` so CACHED groups collapse
275+
// immediately on first paint, even when revalidation is slow or fails; moving it into
276+
// the load() success path would render cached providers expanded and leave them
277+
// expanded whenever the refresh errors.
274278
useEffect(() => {
275279
if (!needsDefaultCollapseRef.current) return;
276280
if (groups.length === 0) return;
277281
needsDefaultCollapseRef.current = false;
278282
const all = new Set(groups.map(group => group.provider));
283+
// eslint-disable-next-line react-hooks/set-state-in-effect
279284
setCollapsed(all);
280285
writeCollapsedProviders(all);
281286
}, [groups]);

gui/src/pages/api-keys-panels.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ function CopyOnClickTip({
5454
}, []);
5555

5656
useLayoutEffect(() => {
57-
if (!showTip) {
58-
setCoords(null);
59-
return;
60-
}
57+
// No reset on close: the portal and aria-describedby are already gated on showTip,
58+
// and reopening remeasures in this same layout effect before paint. Clearing here
59+
// only forced a second render.
60+
if (!showTip) return;
6161
const update = () => {
6262
const el = anchorRef.current;
6363
if (!el) return;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Surface bucketing for the Logs page, kept out of Logs.tsx so that module exports
3+
* components only — a page module that also exports helpers breaks Fast Refresh
4+
* (react-refresh/only-export-components).
5+
*/
6+
export type LogSurface = "claude" | "claude-desktop" | "grok";
7+
export type LogSurfaceFilter = "all" | "claude" | "codex" | "grok";
8+
9+
/** Match Usage surface buckets: Claude includes Desktop; Codex is untagged. */
10+
export function logMatchesSurface(log: { surface?: LogSurface }, filter: LogSurfaceFilter): boolean {
11+
if (filter === "all") return true;
12+
if (filter === "claude") return log.surface === "claude" || log.surface === "claude-desktop";
13+
if (filter === "grok") return log.surface === "grok";
14+
return log.surface === undefined;
15+
}

gui/tests/logs-surface-filter.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from "bun:test";
2-
import { logMatchesSurface } from "../src/pages/Logs";
2+
import { logMatchesSurface } from "../src/pages/logs-surface-filter";
33

44
test("Logs surface filter matches Usage buckets including Grok", () => {
55
expect(logMatchesSurface({}, "all")).toBe(true);

0 commit comments

Comments
 (0)