Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
51fd30e
feat(usage): add safe ledger retention compaction core
Vocllum Sep 8, 2026
5ef1ba4
test(usage): cover safe ledger retention boundaries
Vocllum Sep 8, 2026
5dd4e81
feat(usage): persist strict ledger retention policy
Vocllum Sep 8, 2026
5963d42
feat(usage): move ledger compaction preparation to worker
Vocllum Sep 8, 2026
3a7e126
feat(usage): add background ledger retention job
Vocllum Sep 8, 2026
94cfeb3
feat(usage): schedule background ledger retention checks
Vocllum Sep 8, 2026
fb3f7bb
feat(usage): wire retention into server lifecycle
Vocllum Sep 8, 2026
b8ecf64
feat(storage): expose usage ledger retention controls
Vocllum Sep 8, 2026
590a80a
feat(storage): add usage history size limit panel
Vocllum Sep 8, 2026
e13fa24
feat(storage): surface usage retention in storage workspace
Vocllum Sep 8, 2026
ae1de71
test(usage): cover retention config and commit races
Vocllum Sep 8, 2026
5174875
fix(usage): preserve configured ceiling while retention is off
Vocllum Sep 8, 2026
b94e2f8
feat(storage): declare usage retention management routes
Vocllum Sep 8, 2026
96d52d4
feat(storage): add usage history limit CLI
Vocllum Sep 8, 2026
a492ecb
test(storage): cover usage history limit CLI
Vocllum Sep 8, 2026
d7fa8a9
feat(storage): declare usage history limit capability
Vocllum Sep 8, 2026
c5ec00a
fix(usage): join retention worker during abort
Vocllum Sep 8, 2026
df690d8
feat(storage): tag usage retention atomic replaces
Vocllum Sep 8, 2026
ffbf2f8
fix(usage): use Windows-tolerant atomic ledger replace
Vocllum Sep 8, 2026
e2c1e0d
fix(storage): keep policy save separate from immediate trim
Vocllum Sep 8, 2026
b5fea12
fix(usage): preserve exact row boundaries and owned candidates
Vocllum Sep 8, 2026
e796a64
fix(usage): pass parent-owned retention candidate path
Vocllum Sep 8, 2026
d1cd585
fix(usage): invalidate stale retention runs and clean candidates
Vocllum Sep 8, 2026
3352d90
fix(storage): invalidate retention work after policy changes
Vocllum Sep 8, 2026
2b6b10c
test(usage): cover exact retention row boundaries
Vocllum Sep 8, 2026
cc473b6
feat(gui): localize usage retention controls
Vocllum Sep 8, 2026
9878f3d
feat(gui): register usage retention translations
Vocllum Sep 8, 2026
955c4a7
fix(gui): route usage retention copy through i18n
Vocllum Sep 8, 2026
d2d587d
docs(cli): preserve storage safety rationale
Vocllum Sep 8, 2026
bc228b1
docs(usage): document retention scheduler helpers
Vocllum Sep 8, 2026
040d4fc
docs(server): document shared background lifecycle helpers
Vocllum Sep 8, 2026
2420126
fix(usage): discard stale history projection after retention
Vocllum Sep 8, 2026
e27e3fc
fix(usage): parameterize derived history cleanup
Vocllum Sep 8, 2026
a5edf91
fix(usage): reclaim derived history index after retention
Vocllum Sep 8, 2026
e8b1df8
test(usage): cover derived history cleanup ordering
Vocllum Sep 8, 2026
f06f1f3
fix(usage): preserve history db when sidecar cleanup is blocked
Vocllum Sep 8, 2026
47f7e45
fix(storage): gate retention apply on saved policy
Vocllum Sep 8, 2026
4b3f2a7
fix(storage): prevent applying unsaved usage limit
Vocllum Sep 8, 2026
00aa097
test: cover usage retention replace publisher
Vocllum Sep 8, 2026
050b00a
test(usage): cover stale retention policy generation
Vocllum Sep 8, 2026
ede183e
fix(config): type and validate usage ledger retention
Vocllum Sep 8, 2026
71694ef
docs(cli): document usage ledger retention surface
Vocllum Sep 8, 2026
e100054
fix(gui): localize retention units and type fallback
Vocllum Sep 8, 2026
2776903
test(gui): separate base and overlay locale contracts
Vocllum Sep 8, 2026
3bd1c54
feat(gui): compact usage retention controls
Vocllum Sep 8, 2026
f469735
fix(gui): label retention preset controls
Vocllum Sep 8, 2026
1e29707
fix(storage): reconcile retention route parity
Vocllum Sep 8, 2026
ed57484
fix(gui): avoid new retention lint suppression
Vocllum Sep 8, 2026
1edcc06
feat(usage): streamline ledger retention controls
Vocllum Sep 8, 2026
b22444f
docs(pr): add Usage retention preview
Vocllum Sep 8, 2026
27304bd
chore(gui): drop unused retention overlay plumbing
Vocllum Sep 8, 2026
bb62249
test(usage): cover Unlimited default
Vocllum Sep 8, 2026
3d67220
fix(usage): reflect custom retention draft
Vocllum Sep 8, 2026
3802922
refactor(usage): reduce retention UI to one toggle
Vocllum Sep 8, 2026
0cd5e0e
test(gui): exercise usage retention toggle
Vocllum Sep 8, 2026
8b60d22
docs(pr): remove synthetic usage preview
Vocllum Sep 8, 2026
97f3185
style(usage): match compact retention toggle
Vocllum Sep 8, 2026
71276df
fix(usage): mirror disabled cap semantics
Vocllum Sep 8, 2026
5bb19c0
fix(usage): dim inactive saved ceiling
Vocllum Sep 8, 2026
53cbcc1
test(usage): cover disabled ceiling and stale reads
Vocllum Sep 8, 2026
4712048
feat(usage): default saved ceiling to 1 GiB
Vocllum Sep 8, 2026
9ae5c69
Merge 471204864c5363a910a450c0341180700e73039e into 02bc10e8af1c14476…
Vocllum Sep 8, 2026
91afbe0
Merge upstream dev into feat/usage-ledger-retention-v2
Vocllum Sep 8, 2026
e890123
fix(gui): ignore stale retention read failures
Vocllum Sep 8, 2026
859e073
test(gui): cover stale retention read failures
Vocllum Sep 8, 2026
4c3727a
feat(usage): add retention limit editor
Vocllum Sep 8, 2026
f33810e
fix(usage): keep retention stepper edits committable
Vocllum Sep 8, 2026
199aa92
fix(usage): scope retention editor field styles
Vocllum Sep 8, 2026
3767626
docs(usage): align retained ceiling example
Vocllum Sep 8, 2026
b430aa0
test(usage): cover stepper blur persistence
Vocllum Sep 8, 2026
1870fca
docs(usage): keep unrelated JSON-shape wording
Vocllum Sep 8, 2026
1f147e2
docs(usage): document remembered 1 GiB ceiling
Vocllum Sep 8, 2026
41c19fb
docs(pr): add real usage retention screenshot
Vocllum Sep 9, 2026
7ddb3d6
chore(pr): keep runtime screenshot out of tree
Vocllum Sep 9, 2026
0e6637c
Merge latest dev into usage retention branch
Vocllum Sep 9, 2026
ee4f177
Merge pull request #3 from Vocllum/sync/dev-4042-260909b
Vocllum Sep 9, 2026
1f45d34
Merge latest dev into usage retention
Vocllum Sep 9, 2026
9dd95cd
test(usage): fold retention coverage into usage domain
Vocllum Sep 9, 2026
00e2233
test(cli): fold usage-limit coverage into storage suite
Vocllum Sep 9, 2026
018a26e
test(cli): preserve storage coverage while adding usage-limit cases
Vocllum Sep 9, 2026
7f2b28d
test: remove unregistered retention test basename
Vocllum Sep 9, 2026
693eefb
test: remove unregistered usage-limit test basename
Vocllum Sep 9, 2026
097a8ae
test(usage): preserve scanner unicode fixture
Vocllum Sep 9, 2026
3cabbcc
Merge pull request #6 from Vocllum/sync/dev-4042-260909c
Vocllum Sep 9, 2026
de18ad3
Merge current upstream dev into usage retention
Vocllum Sep 10, 2026
cbea1de
test(gui): include usage retention stepper locale keys
Vocllum Sep 10, 2026
2f4d8ff
chore(skills): regenerate ocx management surface
Vocllum Sep 10, 2026
175df71
fix(skills): preserve generated surface formatting
Vocllum Sep 10, 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
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ badge or the version value to read the full value.
| **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. |
| **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. |
| **Logs** | Auto-refresh recent requests with tokens, requested effort and (when available) effective outbound effort, resolved model, provider, status, request id, duration, and error details. The detail view includes the exact reasoning wire field when the adapter emits one. Filter by opaque conversation/session id (when the client sends one) to total tokens and estimated list-price cost for the currently loaded Logs ring. |
| **Usage / Debug** | Inspect token-usage coverage and trends, or enable opt-in provider transport and usage-extraction diagnostics. |
| **Usage / Debug** | Inspect token-usage coverage and trends. The **Usage** page also exposes the opt-in size ceiling for `usage.jsonl` (`usageLedgerRetention.enabled` / `maxBytes`); the background scheduler compacts older complete rows when the ceiling is exceeded. Enable provider transport and usage-extraction diagnostics here as needed. |
| **Storage** | Read-only CODEX_HOME disk breakdown (sessions, archives, DBs, attachments). Optional archived cleanup: preview the oldest N%, then quarantine to `CODEX_HOME/.trash` (default) or permanently delete behind an explicit checkbox. **Auto-cleanup policy** is opt-in and **default OFF** (`storageCleanupPolicy.enabled`); configure threshold/target/schedule/mode on the Storage page, or trigger **Run now**. Quarantined entries can be restored from the Storage page (JSONL + threads). Active sessions stay read-only. Cleanup and restore are refused while Codex holds the newest/active `state_*.sqlite` locked. |
| **Stop** | Gracefully stop the proxy and installed background service, restore native Codex, and exit (`POST /api/stop`). On Windows with the Task Scheduler backend the dashboard refuses and asks you to run `ocx stop` instead: that wrapper can respawn the proxy after the task ends, and only a stop running outside this process can verify the restart window before restoring your client config. Nothing is changed when it refuses. |

Expand Down
15 changes: 15 additions & 0 deletions docs-site/src/content/docs/reference/cli/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ separately, and requests with no matching price row are counted as
ocx usage --range today --provider xai
```

### `ocx storage usage-limit`

Inspect or change the opt-in `usage.jsonl` size ceiling. The setting is also available in the
dashboard on the **Usage** page.

```bash
ocx storage usage-limit show --json
ocx storage usage-limit set --enabled true --mib 512 --json
```

`set` sends only the fields supplied, so changing `--mib` preserves the saved enabled state.
The minimum ceiling is 1 MiB. A bare `usage-limit` invocation is read-only. The background
scheduler compacts complete JSONL rows after the ledger exceeds the configured ceiling. The
command drives `GET`/`PUT /api/storage/usage-ledger-retention` on the running proxy.

### `ocx debug <provider|usage|injection|claude> <on|off|status|reset|logs [-f]>`

Read or change runtime debug overrides through the running proxy's management API.
Expand Down
33 changes: 33 additions & 0 deletions docs-site/src/content/docs/reference/configuration/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ runs helper features around provider requests.
| `corsAllowOrigins?` | `string[]` | `[]` | Additional exact origins allowed by CORS. Loopback origins are always allowed. Authority-based browser extension origins such as `chrome-extension://<extension-id>` are supported; `*` is not a wildcard. Firefox and Safari regenerate the extension UUID (per install / per browser launch), so update the entry when the origin changes. |
| `apiKeys?` | `OcxApiKey[]` | `[]` | Generated `ocx_…` credentials accepted by management and data-plane auth on non-loopback binds. Dashboard-managed. |
| `storageCleanupPolicy?` | `StorageCleanupPolicy` | disabled | Opt-in archived-session cleanup policy. Never enabled implicitly. |
| `usageLedgerRetention?` | `UsageLedgerRetentionConfig` | disabled | Opt-in cap for the append-only `usage.jsonl` usage ledger. When enabled, the background scheduler compacts complete rows after the ledger exceeds `maxBytes`. |
| `appOwnedMemoryBudgetMb?` | `number` | `256` | Cap in MiB for evictable app-owned logs, caches, blobs, and continuation payloads. Range 64–4096; not an RSS cap. |
| `codexAutoStart?` | `boolean` | `true` | Let the Codex shim run `ocx ensure` before launching Codex. False makes ensure a no-op. |
| `codexShimAutoRestore?` | `boolean` | `true` | Restore an installed shim after a completed external Codex update replaces it. Environment opt-out: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`. |
Expand Down Expand Up @@ -259,6 +260,38 @@ either `target.reduceToBytes` or `target.removeOldestPercent`. `mode` defaults t
Configure it on the Storage page or with `GET`/`PUT /api/storage/cleanup-policy`; trigger a manual run
with `POST /api/storage/cleanup-policy/run`.

## Usage-ledger retention

`usageLedgerRetention` is disabled by default. It is an opt-in size ceiling for
`$OPENCODEX_HOME/usage.jsonl`, the append-only usage ledger used by the Usage page and
`GET /api/usage`. Enabling it lets the proxy compact older rows in a background Worker once
the file exceeds the configured limit; normal request handling is not blocked by the scan.

```json
{
"usageLedgerRetention": {
"enabled": false
}
}
```

The effective default is **Unlimited** (`enabled: false`). An unconfigured installation remembers
**1 GiB** (`1073741824`) as `maxBytes`; that saved value becomes effective on first enable. Set
`maxBytes` explicitly only when selecting a different ceiling. Any explicit value must be a safe
integer of at least 1 MiB (`1048576`). A saved ceiling is retained when `enabled` is set to `false`,
so an operator can pause retention without losing the selected limit. Unknown keys and malformed
values fail closed and leave retention disabled.

Compaction publishes a complete JSONL-row candidate only after the source revision and active-turn
checks still match. An unterminated crash tail is discarded; a single row larger than the ceiling
is dropped so the published ledger remains bounded. The derived request-history projection is
recreated after a successful publish. A policy change invalidates an in-flight candidate, and a
source append during scanning defers the commit for a later run.

The dashboard exposes this control on the **Usage** page. For headless operation, use
`ocx storage usage-limit` or the `GET`/`PUT` management routes below. Setting the policy is
non-destructive; the background scheduler compacts older rows when the ceiling is exceeded.

## Quota-reset notifications (`quotaResetNotify`)

Off by default. When the section is absent, no detection runs, no timer starts, and no state
Expand Down
9 changes: 9 additions & 0 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ by the current window size.
| `GET /api/claude/inbound-debug` | Read Claude inbound debug state and entries | — |
| `GET /api/usage` | Stream the complete usage ledger into compact aggregates, then incrementally fold verified appends; summarize by preset or inclusive custom window and client surface, with a Codex `accounts` breakdown keyed by stable non-PII log labels | 400 invalid custom bounds; returns an `error: "read_failed"` summary if storage cannot be read |
| `GET /api/storage` | Scan Codex storage usage by bucket | Returns an `error: "scan_failed"` payload on scan failure |
| `GET /api/storage/usage-ledger-retention` | Read the usage-ledger retention policy, current `usage.jsonl` size, over-limit state, and the last background job state | — |
| `PUT /api/storage/usage-ledger-retention` | Replace the retention fields supplied in `{ "enabled"?: boolean, "maxBytes"?: integer }`; omitted fields keep their saved values | 400 malformed body, unknown field, or `maxBytes` below 1 MiB; 500 `config_write_failed` |
| `POST /api/storage/cleanup/preview` | Preview archived-session cleanup and return a binding digest | 400 `invalid_json` or `invalid_percent` |
| `POST /api/storage/cleanup` | Quarantine or permanently remove the previewed archived set | 400 invalid input; 409 stale/busy/referenced state; 500 filesystem/database failure |
| `GET /api/storage/trash` | List quarantined cleanup entries | 500 `trash_list_failed` |
Expand All @@ -193,6 +195,13 @@ by the current window size.
| `POST /api/storage/cleanup-policy/run` | Start a manual cleanup-policy run | 409 `already_running`; 500 `cleanup_failed` |
| `GET /api/storage/cleanup-policy/test-stream` | Test-only policy stream hook | 404 `not_found` when unavailable |

The retention status response is shaped as `{ enabled, maxBytes, currentBytes, overLimit, job }`;
`job` reports the process-local background state (`idle` or `running`) and the last outcome when
one exists. `PUT` accepts only `enabled` and `maxBytes`, and merges the supplied fields with the
saved policy. It never starts a compaction by itself. The background scheduler queues a Worker
when the ledger exceeds the ceiling; the canonical ledger is replaced only after complete-row,
active-turn, and source-revision checks pass.

New xAI attempts in `usage.jsonl` include a request-time `credentialSource`: `grok-oauth`
for the resolved Grok CLI OAuth transport, or `xai-api-key` for the public xAI API key
transport. This fixed label contains no credential or account identifier. It belongs to
Expand Down
243 changes: 243 additions & 0 deletions gui/src/components/usage/UsageLedgerRetentionControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { clampNumberDraft } from "../../clamp-draft";
import { formatBytes } from "../../format-bytes";
import { useI18n } from "../../i18n/shared";
import { Switch } from "../../ui";
import { NumberStepper } from "../NumberStepper";

const MIB = 1024 ** 2;
const MAX_MIB = Math.floor(Number.MAX_SAFE_INTEGER / MIB);

interface RetentionStatus {
enabled: boolean;
maxBytes: number;
currentBytes?: number;
}

function parseStatus(value: unknown): RetentionStatus {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid_status");
const candidate = value as Record<string, unknown>;
if (typeof candidate.enabled !== "boolean" || typeof candidate.maxBytes !== "number"
|| !Number.isFinite(candidate.maxBytes) || candidate.maxBytes <= 0) {
throw new Error("invalid_status");
}
return {
enabled: candidate.enabled,
maxBytes: candidate.maxBytes,
currentBytes: typeof candidate.currentBytes === "number" && Number.isFinite(candidate.currentBytes)
? candidate.currentBytes
: undefined,
};
}

function formatMaxMiBDraft(maxBytes: number): string {
return Number.isFinite(maxBytes) && maxBytes > 0 ? String(maxBytes / MIB) : "";
}

function parseMaxMiBDraft(raw: string): number | null {
const mib = Number(raw.trim());
if (!Number.isSafeInteger(mib) || mib < 1 || mib > MAX_MIB) return null;
const bytes = mib * MIB;
return Number.isSafeInteger(bytes) ? bytes : null;
}

/**
* Usage-page control for the opt-in usage-ledger byte ceiling.
*
* The dashboard keeps the switch as the primary action and exposes a MiB-aligned
* custom editor only while the policy is enabled. Toggling the switch always
* sends the exact server-reported byte value, so existing non-MiB-aligned values
* can never be rounded or silently rewritten.
*/
export default function UsageLedgerRetentionControl({ apiBase }: { apiBase: string }) {
const { locale, t } = useI18n();
const mibLabel = formatBytes(MIB, locale).replace(/^[\d.,]+\s*/, "");
const [status, setStatus] = useState<RetentionStatus | null>(null);
const [customDraft, setCustomDraft] = useState("");
const [editing, setEditing] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadGeneration = useRef(0);
const inputRef = useRef<HTMLInputElement>(null);

const load = useCallback(async (signal?: AbortSignal) => {
const generation = ++loadGeneration.current;
try {
const response = await fetch(`${apiBase}/api/storage/usage-ledger-retention`, { signal });
if (!response.ok) throw new Error("load_failed");
const next = parseStatus(await response.json());
if (signal?.aborted || generation !== loadGeneration.current) return;
setError(null);
setStatus(next);
setCustomDraft(formatMaxMiBDraft(next.maxBytes));
} catch (errorValue) {
// A successful PUT invalidates reads that started under the old policy. Stale reads
// must be silent whether they eventually succeed, fail HTTP, reject, or parse badly.
if (signal?.aborted || generation !== loadGeneration.current
|| (errorValue as { name?: string })?.name === "AbortError") return;
throw errorValue;
}
}, [apiBase]);

useEffect(() => {
const controller = new AbortController();
const timeout = window.setTimeout(() => {
void load(controller.signal).catch(errorValue => {
if (!controller.signal.aborted && (errorValue as { name?: string })?.name !== "AbortError") {
setError(t("usage.retention.error"));
}
});
}, 0);
return () => {
window.clearTimeout(timeout);
controller.abort();
};
}, [load, t]);

const persist = useCallback(async (nextEnabled: boolean, maxBytes: number) => {
setBusy(true);
setError(null);
try {
const response = await fetch(`${apiBase}/api/storage/usage-ledger-retention`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: nextEnabled, maxBytes }),
});
if (!response.ok) throw new Error("save_failed");
const next = parseStatus(await response.json());
// A GET may have started before this authoritative mutation completed (for example,
// after a locale change). Do not let that older snapshot repaint the saved state.
loadGeneration.current += 1;
setStatus(next);
setCustomDraft(formatMaxMiBDraft(next.maxBytes));
setEditing(false);
} catch {
setError(t("usage.retention.error"));
} finally {
setBusy(false);
}
}, [apiBase, t]);

const toggle = () => {
if (!status || busy) return;
void persist(!status.enabled, status.maxBytes);
};

const saveCustom = () => {
if (!status || busy) return;
// If the saved value is not MiB-aligned, leaving the field untouched must
// not force an unrelated rounding write; the switch path remains exact.
if (customDraft === formatMaxMiBDraft(status.maxBytes)) {
setEditing(false);
return;
}
const nextMaxBytes = parseMaxMiBDraft(customDraft);
if (nextMaxBytes === null) {
setError(t("usage.retention.error"));
return;
}
void persist(true, nextMaxBytes);
};

const resetCustom = () => {
if (!status) return;
setCustomDraft(formatMaxMiBDraft(status.maxBytes));
setEditing(false);
setError(null);
};

return (
<section className="usage-retention-control" data-testid="usage-ledger-retention" aria-labelledby="usage-retention-title">
<div className="usage-retention-heading">
<div>
<h3 id="usage-retention-title" className="h-section">{t("usage.retention.title")}</h3>
<p className="muted text-control">{t("usage.retention.help")}</p>
</div>
<Switch
on={status?.enabled === true}
onClick={toggle}
disabled={busy || status === null}
label={t("usage.retention.enabled")}
/>
</div>

{status?.enabled && (
<div className="usage-retention-editor" aria-busy={busy}>
<label className="usage-retention-limit">
<span className="field-label">{t("usage.retention.limit")}</span>
<span
className="codex-auto-switch-input-wrap"
onBlur={event => {
if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;
if ((event.relatedTarget as HTMLElement | null)?.closest("button.switch")) {
setEditing(false);
return;
}
if (editing) saveCustom();
}}
>
<input
ref={inputRef}
className="input mono codex-auto-switch-input"
type="number"
min={1}
max={MAX_MIB}
step={1}
inputMode="numeric"
value={customDraft}
placeholder={mibLabel}
disabled={busy}
aria-label={t("usage.retention.limit")}
onFocus={() => setEditing(true)}
onChange={event => {
setError(null);
setCustomDraft(event.target.value);
}}
onKeyDown={event => {
if (event.nativeEvent.isComposing || busy) return;
if (event.key === "Enter") {
event.preventDefault();
saveCustom();
} else if (event.key === "Escape") {
event.preventDefault();
resetCustom();
}
}}
/>
<span className="codex-auto-switch-unit" aria-hidden="true">{mibLabel}</span>
<NumberStepper
disabled={busy}
incrementLabel={t("usage.retention.increase")}
decrementLabel={t("usage.retention.decrease")}
onIncrement={() => {
inputRef.current?.focus();
setEditing(true);
setCustomDraft(clampNumberDraft(customDraft, 1, 1, MAX_MIB));
}}
onDecrement={() => {
inputRef.current?.focus();
setEditing(true);
setCustomDraft(clampNumberDraft(customDraft, -1, 1, MAX_MIB));
}}
/>
</span>
</label>
</div>
)}

<p className="muted text-caption usage-retention-current">
{t("usage.retention.current")}: {status?.currentBytes === undefined ? "—" : formatBytes(status.currentBytes, locale)}
{status && (
<>
{" · "}
{!status.enabled && <><span className="usage-retention-state">{t("usage.retention.unlimited")}</span>{" · "}</>}
<span className={`usage-retention-limit${status.enabled ? "" : " is-disabled"}`}>
{t("usage.retention.limit")}: {formatBytes(status.maxBytes, locale)}
</span>
</>
)}
</p>
{error && <p className="err" role="alert">{error}</p>}
</section>
);
}
Loading
Loading