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:
@@ -147,4 +147,48 @@ describe("Sagi widget", () => {
|
||||
// Left-docked → inline left equals the edge margin (16px).
|
||||
expect(btn.style.left).toBe("16px");
|
||||
});
|
||||
|
||||
it("Ctrl/Cmd+B is ignored while typing in a field", () => {
|
||||
renderSagi();
|
||||
const ta = document.createElement("textarea");
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
act(() => {
|
||||
fireEvent.keyDown(ta, { key: "b", ctrlKey: true });
|
||||
});
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
ta.remove();
|
||||
});
|
||||
|
||||
it("Ctrl/Cmd+B is left to the browser while the mascot is hidden", () => {
|
||||
localStorage.setItem("agent-dashboard-sagi-enabled", "false");
|
||||
renderSagi();
|
||||
const ev = new KeyboardEvent("keydown", { key: "b", ctrlKey: true, cancelable: true });
|
||||
window.dispatchEvent(ev);
|
||||
expect(ev.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it("docks bottom-right above the status bar by default", () => {
|
||||
renderSagi();
|
||||
const btn = screen.getByRole("button", { name: /open sagi companion/i }) as HTMLElement;
|
||||
const top = parseFloat(btn.style.top);
|
||||
const size = parseFloat(btn.style.width);
|
||||
// Bottom edge sits above the 24px status bar plus the edge margin.
|
||||
expect(top + size).toBe(window.innerHeight - 16 - 24);
|
||||
});
|
||||
|
||||
it("arrow keys move the focused avatar and persist it", () => {
|
||||
renderSagi();
|
||||
const btn = screen.getByRole("button", { name: /open sagi companion/i }) as HTMLElement;
|
||||
const before = parseFloat(btn.style.top);
|
||||
act(() => {
|
||||
fireEvent.keyDown(btn, { key: "ArrowUp" });
|
||||
});
|
||||
expect(parseFloat(btn.style.top)).toBeLessThan(before);
|
||||
act(() => {
|
||||
fireEvent.keyDown(btn, { key: "ArrowLeft" });
|
||||
});
|
||||
expect(btn.style.left).toBe("16px");
|
||||
expect(JSON.parse(localStorage.getItem("agent-dashboard-sagi-pos") || "{}").side).toBe("left");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user