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 prefsBackup.ts
|
|
* @description Export / import of THIS browser's preferences (theme, language,
|
|
* notification prefs, pet, sidebar, workspace layout …) as one JSON file, for
|
|
* Settings → Data. Only allowlisted `localStorage` keys travel — never the
|
|
* dashboard access token or a cache — and an import writes only those keys,
|
|
* as strings, so a crafted file cannot plant anything else.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
export const PREFS_FORMAT = "agenthub-browser-prefs";
|
|
|
|
const EXACT = new Set([
|
|
"theme",
|
|
"i18nextLng",
|
|
"agent-monitor-notifications",
|
|
"dashboard_tab",
|
|
"sidebar-collapsed",
|
|
"agent-dashboard-sagi-enabled",
|
|
"agent-dashboard-sagi-muted",
|
|
"agent-dashboard-sagi-pos",
|
|
"agenthub-data-scope",
|
|
]);
|
|
|
|
/** Whether a `localStorage` key is a preference that may be exported/imported. */
|
|
export function isPrefKey(key: string): boolean {
|
|
return EXACT.has(key) || key.startsWith("agenthub.");
|
|
}
|
|
|
|
export interface PrefsFile {
|
|
format: typeof PREFS_FORMAT;
|
|
version: 1;
|
|
exported_at: string;
|
|
prefs: Record<string, string>;
|
|
}
|
|
|
|
/** Every preference key this browser holds. */
|
|
export function exportPrefs(): PrefsFile {
|
|
const prefs: Record<string, string> = {};
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const k = localStorage.key(i);
|
|
if (k && isPrefKey(k)) prefs[k] = localStorage.getItem(k) ?? "";
|
|
}
|
|
return { format: PREFS_FORMAT, version: 1, exported_at: new Date().toISOString(), prefs };
|
|
}
|
|
|
|
/** Write a prefs file's allowlisted string values; returns how many. Throws on a foreign file. */
|
|
export function importPrefs(text: string): number {
|
|
const data = JSON.parse(text) as Partial<PrefsFile>;
|
|
if (!data || data.format !== PREFS_FORMAT || typeof data.prefs !== "object" || !data.prefs)
|
|
throw new Error("not an AgentHub preferences file");
|
|
let n = 0;
|
|
for (const [k, v] of Object.entries(data.prefs)) {
|
|
if (!isPrefKey(k) || typeof v !== "string") continue;
|
|
localStorage.setItem(k, v);
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|