diff --git a/__tests__/single-action-queue.test.tsx b/__tests__/single-action-queue.test.tsx
index 3f2e78d4d..ab8dc6ce2 100644
--- a/__tests__/single-action-queue.test.tsx
+++ b/__tests__/single-action-queue.test.tsx
@@ -2,7 +2,7 @@
/**
* Reframe V2, piece 1 - the Single-Action Queue on /focus.
* Locks the one-action-at-a-time contract: the current (first pending) action is
- * shown with Approve / Snooze / Skip mapped to the 3-decision backend, a visible
+ * shown with Approve / Not now / Hide mapped to the 3-decision backend, a visible
* "Sample" badge while the brief is sample-gated, an always-visible "nothing sends"
* assurance, a progress rail, and the finish-line state when nothing is pending.
*/
@@ -70,7 +70,7 @@ describe("SingleActionQueue", () => {
expect(screen.queryByText("Sample")).toBeNull();
});
- it("exposes Approve / Snooze / Skip controls and the nothing-sends assurance", () => {
+ it("exposes Approve / Not now / Hide controls and the nothing-sends assurance", () => {
render(
{
/>,
);
expect(screen.getByRole("button", { name: /Approve: Do a/ })).toBeTruthy();
- expect(screen.getByRole("button", { name: /Snooze: Do a/ })).toBeTruthy();
- expect(screen.getByRole("button", { name: /Skip: Do a/ })).toBeTruthy();
+ expect(screen.getByRole("button", { name: /Not now.*Do a/ })).toBeTruthy();
+ expect(screen.getByRole("button", { name: /Hide.*Do a/ })).toBeTruthy();
expect(screen.getByTestId("single-action-queue").textContent).toMatch(
/nothing sends without your approval/i,
);
});
- it("maps Approve / Snooze / Skip to approved / skipped / dismissed", () => {
+ it("maps Approve / Not now / Hide to approved / skipped / dismissed", () => {
const onDecision = vi.fn();
// Three independent renders so the same first card is present for each click
// (the real parent re-supplies `actions` as decisions land).
for (const [label, expected] of [
[/Approve: Do a/, "approved"],
- [/Snooze: Do a/, "skipped"],
- [/Skip: Do a/, "dismissed"],
+ [/Not now.*Do a/, "skipped"],
+ [/Hide.*Do a/, "dismissed"],
] as const) {
cleanup();
render(
@@ -151,7 +151,7 @@ describe("SingleActionQueue", () => {
expect(onDecision).toHaveBeenCalledWith("a", "approved");
expect(screen.getByText(/1 of 2/)).toBeTruthy();
- expect(screen.getByTestId("saq-announcer").textContent).toMatch(/Saving approval/i);
+ expect(screen.getByTestId("saq-announcer").textContent).toMatch(/Saving approve/i);
expect(screen.getByTestId("saq-announcer").textContent).not.toMatch(/^Approved/i);
});
diff --git a/app/(shell)/focus/FocusModeClient.tsx b/app/(shell)/focus/FocusModeClient.tsx
index 54b3f5d8d..cebf513e5 100644
--- a/app/(shell)/focus/FocusModeClient.tsx
+++ b/app/(shell)/focus/FocusModeClient.tsx
@@ -6,13 +6,13 @@ import {
Check,
ChevronDown,
ChevronRight,
+ Clock,
Eye,
Flame,
LayoutDashboard,
Loader2,
Send,
ShieldCheck,
- SkipForward,
Sparkles,
Sun,
TrendingUp,
@@ -20,6 +20,7 @@ import {
} from "lucide-react";
import { useRouter } from "next/navigation";
import type { RecoveryOpportunity } from "@/lib/pulse/remote-client";
+import { ACTION_VERBS } from "@/lib/focus/action-verbs";
import type { BusinessVertical } from "@/lib/types";
import { getVerticalCopy, formatCurrency } from "@/lib/focus/vertical-copy";
import { useStreak, getStreakTier } from "@/lib/hooks/useStreak";
@@ -1192,7 +1193,7 @@ export function FocusModeClient({
{/* Swipe hint - only shows on first visit, mobile only */}
{!simpleView && !Object.keys(decisions).length && pendingActions.length > 0 && (
- Swipe right to approve. Swipe left to skip for later.
+ Swipe right to approve. Swipe left for {ACTION_VERBS.skipped.label.toLowerCase()}.
)}
@@ -1201,8 +1202,8 @@ export function FocusModeClient({
Tap Approve to send it (after your okay),{" "}
- Snooze to revisit later, or{" "}
- Skip to pass.
+ {ACTION_VERBS.skipped.label} to revisit later, or{" "}
+ {ACTION_VERBS.dismissed.label} to remove it.
Nothing sends without your say-so.
diff --git a/app/(shell)/focus/WalkthroughBanner.tsx b/app/(shell)/focus/WalkthroughBanner.tsx
index 405949667..9844854fc 100644
--- a/app/(shell)/focus/WalkthroughBanner.tsx
+++ b/app/(shell)/focus/WalkthroughBanner.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { ChevronRight, X, Check, SkipForward, Eye, Sparkles } from "lucide-react";
import type { BusinessVertical } from "@/lib/types";
+import { ACTION_VERBS } from "@/lib/focus/action-verbs";
const STORAGE_KEY = "relay-walkthrough-complete";
@@ -21,8 +22,8 @@ function getSteps(clientNoun: string): WalkthroughStep[] {
},
{
icon: ,
- title: "Approve, skip, or dismiss",
- description: "Tap Approve to send outreach on your behalf. Skip to revisit later. Dismiss to remove it. You stay in control. Nothing sends without your OK.",
+ title: `${ACTION_VERBS.approved.label}, ${ACTION_VERBS.skipped.label}, or ${ACTION_VERBS.dismissed.label}`,
+ description: `Tap ${ACTION_VERBS.approved.label} after reviewing. Each card tells you whether approval sends now or drafts for review. ${ACTION_VERBS.skipped.label} means it comes back later; ${ACTION_VERBS.dismissed.label} removes it. You stay in control.`,
},
{
icon: ,
diff --git a/components/focus/SingleActionQueue.tsx b/components/focus/SingleActionQueue.tsx
index d228ae610..1e0dce7c1 100644
--- a/components/focus/SingleActionQueue.tsx
+++ b/components/focus/SingleActionQueue.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
-import { Check, SkipForward, Clock, FlaskConical, ScrollText } from "lucide-react";
+import { Check, X, Clock, FlaskConical, ScrollText } from "lucide-react";
import type { RecoveryOpportunity } from "@/lib/pulse/remote-client";
+import { ACTION_VERBS } from "@/lib/focus/action-verbs";
import {
TrustStrip,
resolveProvenance,
@@ -56,15 +57,15 @@ const EMPTY_FAILED_SET = new Set();
*
* The page an owner lands on shows ONE action to take next, not a wall of panels.
* This is the focused successor to the top-3 OneScreenBrief: it advances through the
- * pending actions one card at a time (Approve / Snooze / Skip), with a progress rail
+ * pending actions one card at a time, with a progress rail
* and an "N of M" counter, so the owner makes one decision, sees the next, and is
* done. Per-card contract truth (source, status, "will not send until you approve")
* stays visible via the shared TrustStrip. When every action is decided it shows the
* finish-line state.
*
- * Decisions map to the existing 3-decision contract (approved / skipped / dismissed);
- * "Snooze" is skipped (defer) and "Skip" is dismissed (not now) - identical mapping to
- * OneScreenBrief so the backend decide endpoint is unchanged.
+ * Decisions map to the existing 3-decision contract (approved / skipped / dismissed).
+ * Labels come from ACTION_VERBS so every owner-facing surface uses the same
+ * words for the same consequence.
*
* Honesty (do not relax):
* - Until the brief is built on the owner's live data, every card carries a visible
@@ -120,7 +121,7 @@ export function SingleActionQueue({
return () => setAuditSubject(null);
}, [currentSubject, setAuditSubject]);
- // Screen-reader narrative: Approve/Snooze/Skip mutate the queue and advance the
+ // Screen-reader narrative: decisions mutate the queue and advance the
// card, so a polite live region announces the decision + what's next. Without it
// a non-sighted owner taps and hears nothing change (the #1 a11y gap on this surface).
const [announcement, setAnnouncement] = useState("");
@@ -130,7 +131,11 @@ export function SingleActionQueue({
if (submitting.has(id) || localInFlightId.current === id) return;
localInFlightId.current = id;
const verb =
- decision === "approved" ? "approval" : decision === "skipped" ? "snooze" : "skip";
+ decision === "approved"
+ ? ACTION_VERBS.approved.label.toLowerCase()
+ : decision === "skipped"
+ ? ACTION_VERBS.skipped.label.toLowerCase()
+ : ACTION_VERBS.dismissed.label.toLowerCase();
setAnnouncement(
`Saving ${verb}. Nothing sends until this is saved.`,
);
@@ -143,7 +148,7 @@ export function SingleActionQueue({
[onDecision, submitting],
);
- // Keyboard shortcuts for the morning ritual: A approve, S snooze, K skip.
+ // Keyboard shortcuts for the morning ritual: A approve, S not now, K hide.
// The brief is a one-tap habit; on desktop the owner clears the queue without
// reaching for the mouse. Ignored while a decision is in flight, when the
// queue is empty, or when the owner is typing / has a control focused (input,
@@ -330,28 +335,36 @@ export function SingleActionQueue({
Approve
+ {/* Answer the owner's scariest question AT her thumb, not three rows
+ up in the TrustStrip. Driven off the same canonical provenance.sends
+ boolean so it can never overclaim a send. */}
+
+ {provenance.sends
+ ? `Approving sends this to ${current.target?.name ?? "your client"}.`
+ : "Approving drafts this for your review — nothing is sent yet."}
+
diff --git a/lib/focus/action-verbs.ts b/lib/focus/action-verbs.ts
new file mode 100644
index 000000000..35b682fce
--- /dev/null
+++ b/lib/focus/action-verbs.ts
@@ -0,0 +1,46 @@
+/**
+ * Canonical decision verbs for EVERY Focus / morning-brief action surface.
+ *
+ * Why this exists (a real, owner-trust bug it fixes): the word "Skip" did
+ * OPPOSITE things on the two screens an owner toggles between —
+ * - Simple Mode (SingleActionQueue): "Skip" -> dismissed (remove for good)
+ * - Full brief (FocusModeClient): "Skip" -> skipped (defer; comes back)
+ * So an owner who learned "Skip" = defer on one screen would, on the other,
+ * permanently drop a real client. For a wellness owner protective of her client
+ * relationships, not knowing whether she just deferred or dropped someone is the
+ * precise hesitation that turns into avoidance. One source of truth keeps the
+ * label -> consequence mapping identical on every surface.
+ *
+ * The three decisions map to the UNCHANGED backend contract; only the
+ * owner-facing labels are canonicalized here.
+ */
+export type ActionDecision = "approved" | "skipped" | "dismissed";
+
+interface ActionVerb {
+ /** The button label — identical on every action surface. */
+ label: string;
+ /** Plain-English consequence (use as aria detail / optional caption). */
+ consequence: string;
+ /** Full aria-label verb phrase (label + consequence, for screen readers). */
+ aria: string;
+}
+
+export const ACTION_VERBS: Record = {
+ approved: {
+ label: "Approve",
+ consequence: "Approve this action.",
+ aria: "Approve",
+ },
+ skipped: {
+ // Defer — it returns in a later brief. (Never "Skip": ambiguous with remove.)
+ label: "Not now",
+ consequence: "Comes back in a later brief.",
+ aria: "Not now (defer — it comes back in a later brief)",
+ },
+ dismissed: {
+ // Remove for good.
+ label: "Hide",
+ consequence: "Removes this for good.",
+ aria: "Hide (remove this for good)",
+ },
+};