feat(FN-1127): add awaiting-approval ntfy notifications
- Extend ntfy event types and default ntfyEvents to include awaiting-approval - Send high-priority "Plan needs approval" notifications for awaiting-approval task updates with deep-link support - Add Notifications settings checkbox handling for awaiting-approval event selection in Settings modal - Expand notifier and settings modal tests for awaiting-approval defaults, toggles, filtering, and deduplication - Update AGENTS.md ntfy documentation and sample config to include the new event
This commit is contained in:
@@ -1679,7 +1679,7 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("in-review") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("in-review") ? current : [...current, "in-review" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "in-review");
|
||||
@@ -1695,7 +1695,7 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("merged") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("merged") ? current : [...current, "merged" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "merged");
|
||||
@@ -1711,7 +1711,7 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("failed") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("failed") ? current : [...current, "failed" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "failed");
|
||||
@@ -1721,6 +1721,22 @@ export function SettingsModal({
|
||||
Task failed
|
||||
</label>
|
||||
<small>When a task fails during execution (high priority)</small>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("awaiting-approval") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("awaiting-approval") ? current : [...current, "awaiting-approval" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "awaiting-approval");
|
||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||
}}
|
||||
/>
|
||||
Plan needs approval
|
||||
</label>
|
||||
<small>When a task specification needs manual approval before execution</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
|
||||
@@ -22,7 +22,7 @@ const defaultSettings: Settings = {
|
||||
defaultPresetBySize: {},
|
||||
ntfyEnabled: false,
|
||||
ntfyTopic: undefined,
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
taskStuckTimeoutMs: undefined,
|
||||
maxStuckKills: 6,
|
||||
runStepsInNewSessions: false,
|
||||
@@ -1570,7 +1570,7 @@ describe("SettingsModal", () => {
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
@@ -1580,6 +1580,22 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(screen.getByLabelText("Task completed (in-review)")).toBeTruthy());
|
||||
expect(screen.getByLabelText("Task merged")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Task failed")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Plan needs approval")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows awaiting-approval checkbox when ntfy is enabled", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Notifications"));
|
||||
expect(screen.getByLabelText("Plan needs approval")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides ntfyEvents checkboxes when ntfy is disabled", async () => {
|
||||
@@ -1590,6 +1606,7 @@ describe("SettingsModal", () => {
|
||||
expect(screen.queryByLabelText("Task completed (in-review)")).toBeNull();
|
||||
expect(screen.queryByLabelText("Task merged")).toBeNull();
|
||||
expect(screen.queryByLabelText("Task failed")).toBeNull();
|
||||
expect(screen.queryByLabelText("Plan needs approval")).toBeNull();
|
||||
});
|
||||
|
||||
it("ntfyEvents checkboxes are all checked by default", async () => {
|
||||
@@ -1606,6 +1623,7 @@ describe("SettingsModal", () => {
|
||||
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Plan needs approval") as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
|
||||
it("saves ntfyEvents correctly when checkboxes are toggled", async () => {
|
||||
@@ -1613,7 +1631,7 @@ describe("SettingsModal", () => {
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
@@ -1629,7 +1647,7 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.ntfyEvents).toEqual(["in-review", "failed"]);
|
||||
expect(payload.ntfyEvents).toEqual(["in-review", "failed", "awaiting-approval"]);
|
||||
});
|
||||
|
||||
it("sets ntfyEvents to undefined when all checkboxes are unchecked", async () => {
|
||||
@@ -1637,7 +1655,7 @@ describe("SettingsModal", () => {
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
@@ -1645,10 +1663,11 @@ describe("SettingsModal", () => {
|
||||
|
||||
fireEvent.click(screen.getByText("Notifications"));
|
||||
|
||||
// Uncheck all three
|
||||
// Uncheck all four
|
||||
fireEvent.click(screen.getByLabelText("Task completed (in-review)"));
|
||||
fireEvent.click(screen.getByLabelText("Task merged"));
|
||||
fireEvent.click(screen.getByLabelText("Task failed"));
|
||||
fireEvent.click(screen.getByLabelText("Plan needs approval"));
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
|
||||
@@ -1672,6 +1691,7 @@ describe("SettingsModal", () => {
|
||||
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(false);
|
||||
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(false);
|
||||
expect((screen.getByLabelText("Plan needs approval") as HTMLInputElement).checked).toBe(false);
|
||||
});
|
||||
|
||||
// Model filter tests with CustomModelDropdown
|
||||
|
||||
Reference in New Issue
Block a user