feat(codex): add opt-in migration and ledger-based restore for unified session history

- Enable dialog gains a checkbox (default off) to migrate existing
  official sessions from the built-in "openai" bucket into the shared
  "custom" bucket, with per-generation backups; failed migrations retry
  at startup
- Disable dialog offers a precise restore driven by the backup ledger:
  only sessions recorded as "openai" in backups are flipped back, and
  sessions created while the toggle was on are never touched
- Completion marker and backup generations are bound to the canonical
  Codex config dir; migrate/restore serialize on an op lock and the
  marker is written conditionally inside the settings write lock
- save_settings rolls back the toggle and fails the save when the live
  rewrite fails; migration additionally requires the live config to
  actually route to the shared bucket (skips with live_not_unified so
  refused injection or proxy takeover can't split history)
- Restore refuses to run while the toggle is (re-)enabled and reports
  nothing_to_restore instead of a zero-count success; local migration
  markers are now backend-owned in merge_settings_for_save so stale
  frontend payloads can't resurrect them
- Settings autosave reverts optimistic form state on failure so a
  failed toggle change can't be replayed by an unrelated save
- ConfirmDialog supports an optional checkbox; all four locales updated
This commit is contained in:
Jason
2026-06-12 23:35:01 +08:00
parent 948d762792
commit eab6bfd20c
15 changed files with 1369 additions and 43 deletions
+31 -2
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
@@ -7,6 +8,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { AlertTriangle, Info } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -18,7 +20,10 @@ interface ConfirmDialogProps {
cancelText?: string;
variant?: "destructive" | "info";
zIndex?: "base" | "nested" | "alert" | "top";
onConfirm: () => void;
/** 可选勾选项:提供 label 即显示,勾选状态经 onConfirm 参数回传 */
checkboxLabel?: string;
checkboxDefaultChecked?: boolean;
onConfirm: (checkboxChecked: boolean) => void;
onCancel: () => void;
}
@@ -30,10 +35,21 @@ export function ConfirmDialog({
cancelText,
variant = "destructive",
zIndex = "alert",
checkboxLabel,
checkboxDefaultChecked = false,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const { t } = useTranslation();
const [checkboxChecked, setCheckboxChecked] = useState(
checkboxDefaultChecked,
);
useEffect(() => {
if (isOpen) {
setCheckboxChecked(checkboxDefaultChecked);
}
}, [isOpen, checkboxDefaultChecked]);
const IconComponent = variant === "info" ? Info : AlertTriangle;
const iconClass =
@@ -58,13 +74,26 @@ export function ConfirmDialog({
{message}
</DialogDescription>
</DialogHeader>
{checkboxLabel ? (
<label className="flex cursor-pointer select-none items-start gap-2 px-6 pt-3">
<Checkbox
checked={checkboxChecked}
onCheckedChange={(value) => setCheckboxChecked(value === true)}
className="mt-0.5"
/>
<span className="text-sm leading-relaxed">{checkboxLabel}</span>
</label>
) : null}
<DialogFooter className="flex gap-2 border-t-0 bg-transparent pt-2 sm:justify-end">
<Button variant="outline" onClick={onCancel}>
{cancelText || t("common.cancel")}
</Button>
<Button
variant={variant === "info" ? "default" : "destructive"}
onClick={onConfirm}
onClick={() =>
// 未渲染勾选框时不得回传 defaultChecked 残留值
onConfirm(checkboxLabel ? checkboxChecked : false)
}
>
{confirmText || t("common.confirm")}
</Button>