6715fbd971
- 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.
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
/**
|
|
* @file configPrimitives.tsx
|
|
* @description Small tool-agnostic atoms shared by the config-explorer UI
|
|
* (Claude Code today, Codex next): an empty-state card, a skeleton-row list
|
|
* placeholder, a copy-to-clipboard button, and a byte-size formatter. None
|
|
* of these know about any particular tool's API.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Copy, Check } from "lucide-react";
|
|
import { copyText } from "../../lib/clipboard";
|
|
|
|
export function CopyButton({ value }: { value: string }) {
|
|
const { t } = useTranslation("ccConfig");
|
|
const [copied, setCopied] = useState(false);
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
if (await copyText(value, true)) {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1500);
|
|
}
|
|
}}
|
|
className="text-[10px] font-medium px-1.5 py-1 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1 flex-shrink-0"
|
|
title={t("common.copyPath")}
|
|
aria-label={t("common.copyPath")}
|
|
>
|
|
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function Empty() {
|
|
const { t } = useTranslation("ccConfig");
|
|
return (
|
|
<div className="rounded-lg border border-dashed border-border bg-surface-2 px-4 py-8 text-center text-sm text-fg-muted">
|
|
{t("common.empty")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SkeletonRows({ n }: { n: number }) {
|
|
return (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: n }).map((_, i) => (
|
|
<div key={i} className="h-16 rounded-lg border border-border bg-surface-2 animate-pulse" />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function formatBytes(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
}
|