feat(settings): data-safe editors, deep-linked search, Data/About/Cost pages
- EditorModal loads once per path (a parent re-render no longer reloads the file over the user's typing), refuses Save after a failed load, opens a truncated (>256 KB) file read-only, and asks before overwriting a file that changed on disk or could not be re-read. - Settings/Hooks editors unset only keys from the snapshot taken at Edit, warn when the file changed underneath, keep a renamed key's value, and refuse a new key named like a redacted one (env) or a read-only hook event, which used to replace the secret/hooks. - Project scope shows its resolved folder and is disabled inside the plugin cache. Account-profile, pricing-row and pet deletes confirm; two-click actions disarm after 3 s; unsaved drafts ask before closing. - Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and scrolls to + highlights the row; the active config tab is in the URL. - General splits into General / Data / About; Pricing moves to a Cost group with one shared load; config tabs group as Content / Extensions / Config with arrow-key roving; Promise.allSettled per tab. - Data: browser-preferences export/import (allowlisted keys), per-section reset, housekeeping dry-run first, clear-all behind a typed phrase. Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and dry_run returns exactly what the real run would delete. - Alerts can be edited; webhooks/alerts/remote-source failures toast; copies go through copyText; modals get dialog roles and useDialog. - Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of row controls, arrow keys move it, reduced motion honoured; the dead /run launch link goes to the workspace.
This commit is contained in:
@@ -6,17 +6,31 @@
|
||||
* uploading an image or importing a `.codex-pet.zip`. Everything here is the
|
||||
* same state and calls as the mascot's right-click menu (`Sagi/usePets.ts`,
|
||||
* `Sagi/prefs.ts`): the choice and size are per browser, the pets live on the
|
||||
* server (`/api/pets`).
|
||||
* server (`/api/pets`). Previews follow the OS reduced-motion preference; size
|
||||
* and bubbles are disabled while the pet is hidden; a delete asks first; and
|
||||
* **Reset to defaults** puts show / bubbles / size / pet back.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useRef, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Check, Download, ExternalLink, Loader2, PackageOpen, Trash2, Upload } from "lucide-react";
|
||||
import {
|
||||
Check,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
PackageOpen,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { SettingsRow, SettingsSwitchRow } from "./SettingsRow";
|
||||
import { SagiAvatar } from "../Sagi/SagiAvatar";
|
||||
import { PetSprite } from "../Sagi/PetSprite";
|
||||
import { PET_SIZE, sagiPrefs } from "../Sagi/prefs";
|
||||
import { usePrefersReducedMotion } from "../Sagi/Sagi";
|
||||
import { confirmDialog } from "../../lib/confirm";
|
||||
import { toast } from "../../lib/toast";
|
||||
import { usePets, useWornPet } from "../Sagi/usePets";
|
||||
import type { PetView } from "../../lib/api";
|
||||
|
||||
@@ -78,7 +92,9 @@ function PetCard({
|
||||
disabled={disabled}
|
||||
aria-label={deleteLabel}
|
||||
title={deleteLabel}
|
||||
className="absolute right-1.5 top-1.5 rounded p-1 text-fg-muted opacity-0 transition-opacity hover:bg-surface-4 hover:text-status-danger focus:opacity-100 group-hover:opacity-100"
|
||||
className={`absolute right-1.5 top-1.5 rounded p-1 text-fg-muted transition-opacity hover:bg-surface-4 hover:text-status-danger focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 ${
|
||||
selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -88,13 +104,14 @@ function PetCard({
|
||||
}
|
||||
|
||||
function Preview({ pet, size }: { pet: PetView; size: number }) {
|
||||
const reducedMotion = usePrefersReducedMotion();
|
||||
return (
|
||||
<PetSprite
|
||||
id={pet.id}
|
||||
kind={pet.kind}
|
||||
mood="idle"
|
||||
size={size}
|
||||
reducedMotion={false}
|
||||
reducedMotion={reducedMotion}
|
||||
onBroken={() => {}}
|
||||
/>
|
||||
);
|
||||
@@ -108,6 +125,22 @@ export function PetSettings() {
|
||||
const bundleInput = useRef<HTMLInputElement>(null);
|
||||
const wornPet = list?.pets.find((p) => p.id === worn.pet.id);
|
||||
const sizePreview = Math.min(worn.size, 120);
|
||||
const reducedMotion = usePrefersReducedMotion();
|
||||
|
||||
const resetDefaults = async () => {
|
||||
const ok = await confirmDialog({
|
||||
title: t("sagi.pet.reset"),
|
||||
message: t("sagi.pet.resetMessage"),
|
||||
confirmLabel: t("sagi.pet.reset"),
|
||||
destructive: false,
|
||||
});
|
||||
if (!ok) return;
|
||||
sagiPrefs.setEnabled(true);
|
||||
sagiPrefs.setMuted(false);
|
||||
sagiPrefs.setSize(PET_SIZE.default);
|
||||
sagiPrefs.setPet({ id: "sagi" });
|
||||
toast.success(t("sagi.pet.resetDone"));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -123,6 +156,7 @@ export function PetSettings() {
|
||||
onChange={(v) => sagiPrefs.setMuted(!v)}
|
||||
label={t("sagi.bubbles")}
|
||||
description={t("sagi.bubblesDesc")}
|
||||
disabled={!worn.shown}
|
||||
/>
|
||||
<SettingsRow
|
||||
label={t("sagi.pet.size")}
|
||||
@@ -136,8 +170,9 @@ export function PetSettings() {
|
||||
min={PET_SIZE.min}
|
||||
max={PET_SIZE.max}
|
||||
value={worn.size}
|
||||
disabled={!worn.shown}
|
||||
onChange={(e) => sagiPrefs.setSize(Number(e.target.value))}
|
||||
className="w-40 accent-[rgb(var(--accent))]"
|
||||
className="w-40 accent-[rgb(var(--accent))] disabled:opacity-50"
|
||||
/>
|
||||
<span className="w-12 text-right text-xs tabular-nums text-fg-muted">
|
||||
{worn.size}px
|
||||
@@ -152,9 +187,23 @@ export function PetSettings() {
|
||||
{wornPet ? (
|
||||
<Preview pet={wornPet} size={sizePreview} />
|
||||
) : (
|
||||
<SagiAvatar mood="idle" reducedMotion={false} size={sizePreview} />
|
||||
<SagiAvatar mood="idle" reducedMotion={reducedMotion} size={sizePreview} />
|
||||
)}
|
||||
</div>
|
||||
{worn.size > 120 && (
|
||||
<p className="mt-1.5 text-[11px] text-fg-muted">
|
||||
{t("sagi.pet.previewCapped", { size: worn.size })}
|
||||
</p>
|
||||
)}
|
||||
{!worn.shown && (
|
||||
<p className="mt-1.5 text-[11px] text-fg-muted">{t("sagi.pet.hiddenNote")}</p>
|
||||
)}
|
||||
<div className="mt-3 flex justify-end">
|
||||
<button type="button" className="btn-ghost text-xs" onClick={() => void resetDefaults()}>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
{t("sagi.pet.reset")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5">
|
||||
|
||||
Reference in New Issue
Block a user