feat(settings): a Pet page for the mascot

Settings -> Pet (/settings/pet, PetSettings.tsx): show/hide, speech
bubbles, size with a live preview, and the pet grid - Sagi, featured
codex-pets.net pets (downloaded on first pick), your own (deletable) -
with image upload and .codex-pet.zip import. The Sagi block leaves
General; the mascot menu's Pet settings opens the page. PetMenu and the
page share components/Sagi/usePets.ts.
This commit is contained in:
2026-10-05 09:54:36 +07:00
parent ad6e80192d
commit 151dc8a480
13 changed files with 463 additions and 196 deletions
+1 -51
View File
@@ -1,6 +1,6 @@
/**
* @file Settings.tsx
* @description The install-wide settings, as two pages of the Settings nav (`SettingsHub.tsx`): `section="general"` — total cost, workspaces housekeeping, import, remote sources, Sagi, data management and system information — and `section="notifications"` — browser notifications and alerts. Appearance is its own page (`components/settings/AppearanceSettings.tsx`); per-tool settings live in that tool's page under `/settings/agents/<id>`.
* @description The install-wide settings, as two pages of the Settings nav (`SettingsHub.tsx`): `section="general"` — total cost, workspaces housekeeping, import, remote sources, data management and system information (Sagi has its own page, `components/settings/PetSettings.tsx`) — and `section="notifications"` — browser notifications and alerts. Appearance is its own page (`components/settings/AppearanceSettings.tsx`); per-tool settings live in that tool's page under `/settings/agents/<id>`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
@@ -23,7 +23,6 @@
* ## Internal dependencies
* - `../lib/api`
* - `../lib/eventBus`
* - `../components/Sagi/prefs`
* - `../lib/format`
* - `../lib/push`
* - `../components/Tip`
@@ -90,7 +89,6 @@ import {
import { api } from "../lib/api";
import { eventBus } from "../lib/eventBus";
import { isRemoteDataRefreshMessage } from "../lib/remoteDataEvents";
import { sagiPrefs } from "../components/Sagi/prefs";
import { fmt, fmtCost } from "../lib/format";
import { subscribeToPush, unsubscribeFromPush } from "../lib/push";
import {
@@ -209,11 +207,6 @@ export function Settings({
} | null>(null);
const [confirmAction, setConfirmAction] = useState<string | null>(null);
const [notifPrefs, setNotifPrefs] = useState<NotifPrefs>(loadNotifPrefs);
const [sagiEnabled, setSagiEnabled] = useState(() => sagiPrefs.getEnabled());
const setSagi = useCallback((v: boolean) => {
sagiPrefs.setEnabled(v);
setSagiEnabled(v);
}, []);
const [abandonHours, setAbandonHours] = useState("24");
const [purgeDays, setPurgeDays] = useState("90");
const [gcDryRun, setGcDryRun] = useState(false);
@@ -458,49 +451,6 @@ export function Settings({
<section id="remote-sources" className="scroll-mt-24">
<RemoteSources />
</section>
{/* ─── SAGI COMPANION ─── */}
<section id="sagi" className="scroll-mt-24">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<span className="text-base leading-none" aria-hidden>
🦸
</span>
{t("sagi.title", "Sagi companion")}
</h3>
<p className="text-xs text-fg-muted mb-4">
{t("sagi.description", "A floating mascot that reacts to your live sessions.")}
</p>
<div className="card p-5">
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${
sagiEnabled
? "bg-blue-600/10 border border-blue-600/20"
: "bg-surface-2 border border-border"
}`}
>
<span className="text-lg leading-none" aria-hidden>
🦸
</span>
</div>
<div className="min-w-0 flex-1">
<SettingsSwitchRow
checked={sagiEnabled}
onChange={setSagi}
label={t("sagi.enable", "Show Sagi")}
description={t(
"sagi.enableDesc",
"Display the corner companion across the dashboard (⌘B to open)"
)}
/>
</div>
</div>
<p data-testid="sagi-pet-hint" className="mt-3 text-xs text-fg-muted">
{t("sagi.petHint")}
</p>
</div>
</section>
</>
)}
+9
View File
@@ -24,6 +24,7 @@ import {
KeyRound,
Orbit,
Palette,
PawPrint,
Puzzle,
Router,
Search,
@@ -37,6 +38,7 @@ import { AiToolConfig } from "./AiToolConfig";
import { AiRouterConfig } from "./AiRouterConfig";
import { GitCredentialsConfig } from "./GitCredentialsConfig";
import { AppearanceSettings } from "../components/settings/AppearanceSettings";
import { PetSettings } from "../components/settings/PetSettings";
import { AiToolsSettings } from "../components/settings/AiToolsSettings";
import { PricingSettings } from "../components/settings/PricingSettings";
import { WorktreeVisibilitySettings } from "../components/settings/WorktreeVisibilitySettings";
@@ -106,6 +108,13 @@ export function SettingsHub() {
...s("appearance"),
render: () => <AppearanceSettings />,
},
{
id: "pet",
group: "general",
Icon: PawPrint,
...s("pet"),
render: () => <PetSettings />,
},
{
id: "notifications",
group: "general",
@@ -7678,89 +7678,6 @@ exports[`screen snapshots > Settings 1`] = `
</div>
</div>
</section>
<section
class="scroll-mt-24"
id="sagi"
>
<h3
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
>
<span
aria-hidden="true"
class="text-base leading-none"
>
🦸
</span>
Sagi companion
</h3>
<p
class="text-xs text-fg-muted mb-4"
>
A floating mascot that reacts to your live sessions.
</p>
<div
class="card p-5"
>
<div
class="flex items-center gap-3"
>
<div
class="w-10 h-10 rounded-xl flex items-center justify-center transition-colors bg-blue-600/10 border border-blue-600/20"
>
<span
aria-hidden="true"
class="text-lg leading-none"
>
🦸
</span>
</div>
<div
class="min-w-0 flex-1"
>
<div
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
>
<div
class="min-w-0"
>
<p
class="text-sm text-fg-secondary"
>
Show Sagi
</p>
<p
class="mt-0.5 text-xs text-fg-muted"
>
Display the corner companion across the dashboard (⌘B to open)
</p>
</div>
<div
class="shrink-0"
>
<button
aria-checked="true"
aria-label="Show Sagi"
class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-50 bg-accent"
role="switch"
type="button"
>
<span
aria-hidden="true"
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-transform translate-x-[18px]"
/>
</button>
</div>
</div>
</div>
</div>
<p
class="mt-3 text-xs text-fg-muted"
data-testid="sagi-pet-hint"
>
Right-click the mascot to choose its pet — Sagi, a featured or imported Codex pet, or your own image — and its size.
</p>
</div>
</section>
<section
class="scroll-mt-24"
id="data"