# Files changed · PR \#7 · flambo/t3code

[View on GitCafe](https://git.cafe/flambo/t3code/pull/7/diffs)

Repository: [flambo/t3code](https://git.cafe/flambo/t3code)

Visibility: public

[feat\(web\): snooze a thread until its pull request needs attention](https://git.cafe/flambo/t3code/pull/7.md)

Version: 1

Head commit: 12caa30eb8cfc90b782f9ed7ac0cf7c290238d7d

Base commit: 91256f4c1b30532f8d85156d7378a6dcf4a6f22e

Comparison base: 91256f4c1b30532f8d85156d7378a6dcf4a6f22e

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/ChatView.logic.test.ts

Status: modified; +57; −1

```diff
@@ -1,5 +1,10 @@
-import { findRecordedWorktreeSetup, resolveVisibleWorktreeSetup } from "./ChatView.logic";
 import {
+  composerBackgroundTasks,
+  findRecordedWorktreeSetup,
+  resolveVisibleWorktreeSetup,
+} from "./ChatView.logic";
+import { derivePendingBackgroundWork } from "@t3tools/shared/orchestrationV2PendingBackgroundWork";
+import {
   recallCheckoutIsRepo,
   rememberCheckoutIsRepo,
   threadShellHasStarted,
@@ -2180,3 +2185,54 @@
     vi.useRealTimers();
   });
 });
+
+describe("composerBackgroundTasks", () => {
+  const tasks = derivePendingBackgroundWork({
+    latestRun: { id: RunId.make("run-1"), ordinal: 1, status: "completed" },
+    providerThreads: [
+      {
+        id: "provider-thread-1" as never,
+        pendingBackgroundTasks: [
+          { taskId: "agent-1", kind: "subagent", description: "Explore" },
+          { taskId: "tail-logs", kind: "monitor", description: "Tail logs" },
+        ],
+      },
+    ],
+    turnItems: [],
+    pullRequests: [
+      {
+        host: "github.com",
+        repository: "acme/app",
+        number: 7,
+        url: "https://github.com/acme/app/pull/7",
+        source: "agent",
+        watch: {
+          startedAt: "2026-10-05T00:00:00.000Z",
+          headSha: null,
+          failedChecks: [],
+          passed: false,
+          passedChecks: [],
+          remarksThrough: "2026-10-05T00:00:00.000Z",
+          remarkIds: [],
+          conflicting: false,
+          wakes: 0,
+        },
+      },
+    ],
+  });
+
+  it("hides only pull request watches while the thread waits on its pull request", () => {
+    expect(composerBackgroundTasks(tasks, true).map((task) => task.taskId)).toEqual([
+      "agent-1",
+      "tail-logs",
+    ]);
+  });
+
+  it("keeps every task for awake and timed-snoozed threads", () => {
+    expect(composerBackgroundTasks(tasks, false).map((task) => task.taskId)).toEqual([
+      "agent-1",
+      "tail-logs",
+      "pull-request-watch:github.com/acme/app#7",
+    ]);
+  });
+});
```

## apps/web/src/components/ChatView.logic.ts

Status: modified; +16; −0

```diff
@@ -34,6 +34,10 @@
   type AtomCommandResult,
 } from "@t3tools/client-runtime/state/runtime";
 import { videoMimeType } from "@t3tools/shared/video";
+import {
+  isPullRequestWatchTask,
+  type PendingBackgroundWorkTask,
+} from "@t3tools/shared/orchestrationV2PendingBackgroundWork";
 import {
   appendCodexArtifactTemplateUsePrompt,
   codexArtifactTemplateUsePrompt,
@@ -1345,3 +1349,15 @@
     detectTrigger: true,
   });
 }
+
+/**
+ * Background work for the composer banner. An attention-snoozed thread's
+ * status line is its visible state, so its pull request watches stay in the
+ * thread details card instead of the banner.
+ */
+export function composerBackgroundTasks(
+  tasks: ReadonlyArray<PendingBackgroundWorkTask>,
+  snoozedUntilAttention: boolean,
+): ReadonlyArray<PendingBackgroundWorkTask> {
+  return snoozedUntilAttention ? tasks.filter((task) => !isPullRequestWatchTask(task)) : tasks;
+}
```

## apps/web/src/components/ChatView.tsx

Status: modified; +17; −5

```diff
@@ -8,7 +8,7 @@
   resolveWorktreeSetupProgress,
 } from "./ChatView.logic";
 import * as DateTime from "effect/DateTime";
-import { restorePlanFollowUpComposer } from "./ChatView.logic";
+import { composerBackgroundTasks, restorePlanFollowUpComposer } from "./ChatView.logic";
 import { assistantCitationsToPlainText } from "@t3tools/shared/assistantCitations";
 import { prepareQueuedEditAttachments, recoverQueuedMessageEdit } from "./chat/queuedMessageEdit";
 import {
@@ -7091,6 +7091,8 @@
     activeThreadShell !== null &&
     supportsSnooze &&
     effectiveSnoozed(activeThreadShell, { now: new Date().toISOString() });
+  const activeThreadSnoozedUntilAttention =
+    activeThreadSnoozed && activeThreadShell?.snoozedUntilNeedsAttention === true;
   const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0);
   useEffect(() => {
     void snoozeWakeTick;
@@ -7341,7 +7343,14 @@
   // the turn; once it settles, the composer stop button is gone, so this
   // banner is the only visible stop affordance. The interrupt path also
   // accepts a completed run while its provider still has background work.
+  const hasActiveThread = activeThread != null;
+  const activeBackgroundTasks = useMemo(
+    () =>
+      !isWorking && hasActiveThread
+        ? composerBackgroundTasks(pendingBackgroundTasks, activeThreadSnoozedUntilAttention)
-  const activeBackgroundTasks = !isWorking && activeThread ? pendingBackgroundTasks : [];
+        : [],
+    [activeThreadSnoozedUntilAttention, hasActiveThread, isWorking, pendingBackgroundTasks],
+  );
   const [stoppingBackgroundWorkKey, setStoppingBackgroundWorkKey] = useState<string | null>(null);
   const isStoppingBackgroundWork =
     stoppingBackgroundWorkKey === `${environmentId}:${activeThreadId}`;
@@ -7577,9 +7586,11 @@
       <ThreadStatusLine
         icon={<AlarmClockIcon />}
         label={
+          activeThreadSnoozedUntilAttention
+            ? "Snoozed until its pull request needs attention"
+            : activeThreadShell?.snoozedUntil
+              ? `Snoozed, ${formatRelativeTimeUntilLabel(activeThreadShell.snoozedUntil)}`
-          activeThreadShell?.snoozedUntil
+              : "Snoozed"
-            ? `Snoozed, ${formatRelativeTimeUntilLabel(activeThreadShell.snoozedUntil)}`
-            : "Snoozed"
         }
         actionLabel={isUnsnoozing ? "Waking..." : "Wake now"}
         actionDisabled={!canOperateThread || isUnsnoozing}
@@ -7611,6 +7622,7 @@
     activeThreadShell?.settledAt,
     activeThreadShell?.snoozedUntil,
     activeThreadSnoozed,
+    activeThreadSnoozedUntilAttention,
     activeThreadWokeVisible,
     canOperateThread,
     handleUnsettleActiveThread,
```

## apps/web/src/components/CommandPalette.logic.test.ts

Status: modified; +62; −0

```diff
@@ -14,6 +14,7 @@
   filterCommandPaletteGroups,
   findHighlightedCommandPaletteItem,
   reduceCommandPaletteUiState,
+  resolveActiveThreadSnoozeAction,
   type CommandPaletteActionItem,
   type CommandPaletteGroup,
 } from "./CommandPalette.logic";
@@ -894,3 +895,64 @@
     expect(findHighlightedCommandPaletteItem(groups, null)).toBeNull();
   });
 });
+
+describe("resolveActiveThreadSnoozeAction", () => {
+  const NOW = "2026-10-06T22:30:00.000Z";
+  const thread = (snooze: { snoozedUntil?: string; snoozedUntilNeedsAttention?: boolean } = {}) =>
+    makeThreadFixture({
+      snoozedAt: snooze.snoozedUntil || snooze.snoozedUntilNeedsAttention ? NOW : null,
+      snoozedUntil: snooze.snoozedUntil ?? null,
+      snoozedUntilNeedsAttention: snooze.snoozedUntilNeedsAttention ?? false,
+      pullRequests: [
+        {
+          host: "github.com",
+          repository: "acme/app",
+          number: 1,
+          url: "https://github.com/acme/app/pull/1",
+          source: "agent",
+          linkedAt: NOW,
+          snapshot: null,
+          stack: null,
+        },
+      ],
+    });
+  const currentServer = { threadSnooze: true, threadSnoozeUntilAttention: true };
+  const timedSnoozeServer = { threadSnooze: true };
+
+  it("offers snoozing until it needs attention only on servers that advertise it", () => {
+    expect(
+      resolveActiveThreadSnoozeAction({ thread: thread(), capabilities: currentServer, now: NOW }),
+    ).toEqual({ kind: "snooze-until-attention", disabled: false });
+    expect(
+      resolveActiveThreadSnoozeAction({
+        thread: thread(),
+        capabilities: timedSnoozeServer,
+        now: NOW,
+      }),
+    ).toBeNull();
+  });
+
+  it("offers waking a snoozed thread, timed or until it needs attention", () => {
+    expect(
+      resolveActiveThreadSnoozeAction({
+        thread: thread({ snoozedUntilNeedsAttention: true }),
+        capabilities: currentServer,
+        now: NOW,
+      }),
+    ).toEqual({ kind: "wake" });
+    expect(
+      resolveActiveThreadSnoozeAction({
+        thread: thread({ snoozedUntil: "2026-10-07T09:00:00.000Z" }),
+        capabilities: timedSnoozeServer,
+        now: NOW,
+      }),
+    ).toEqual({ kind: "wake" });
+    expect(
+      resolveActiveThreadSnoozeAction({
+        thread: thread({ snoozedUntilNeedsAttention: true }),
+        capabilities: {},
+        now: NOW,
+      }),
+    ).toBeNull();
+  });
+});
```

## apps/web/src/components/CommandPalette.logic.ts

Status: modified; +24; −0

```diff
@@ -7,6 +7,11 @@
   THREAD_JUMP_KEYBINDING_COMMANDS,
 } from "@t3tools/contracts";
 import { filterFilesystemBrowseEntries } from "@t3tools/client-runtime/state/filesystem";
+import {
+  canSnooze,
+  effectiveSnoozed,
+  type ThreadSnoozeShell,
+} from "@t3tools/client-runtime/state/thread-settled";
 import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
 import * as Arr from "effect/Array";
 import * as Result from "effect/Result";
@@ -14,6 +19,7 @@
 import { getThreadSortTimestamp, sortThreads } from "../lib/threadSort";
 import { normalizeSearchText } from "../lib/utils";
 import { formatRelativeTimeLabel } from "../timestampFormat";
+import { offersSnoozeUntilAttention } from "./Sidebar.snooze";
 import { type Project, type SidebarThreadSummary, type Thread } from "../types";
 
 export const RECENT_THREAD_LIMIT = 12;
@@ -39,6 +45,24 @@
   }));
 }
 
+/** The snooze action the palette offers for the current thread: wake it when snoozed, otherwise snooze it until it needs attention. */
+export function resolveActiveThreadSnoozeAction(input: {
+  readonly thread: ThreadSnoozeShell & Parameters<typeof offersSnoozeUntilAttention>[0];
+  readonly capabilities:
+    | { readonly threadSnooze?: boolean; readonly threadSnoozeUntilAttention?: boolean }
+    | undefined;
+  readonly now: string;
+}):
+  | { readonly kind: "wake" }
+  | { readonly kind: "snooze-until-attention"; readonly disabled: boolean }
+  | null {
+  const { thread, capabilities, now } = input;
+  if (capabilities?.threadSnooze !== true) return null;
+  if (effectiveSnoozed(thread, { now })) return { kind: "wake" };
+  if (!offersSnoozeUntilAttention(thread, capabilities)) return null;
+  return { kind: "snooze-until-attention", disabled: !canSnooze(thread, { now }) };
+}
+
 export function browseInputEndPaddingClass(input: {
   readonly willCreateProjectPath: boolean;
   readonly hasHighlightedBrowseItem: boolean;
```

## apps/web/src/components/CommandPalette.tsx

Status: modified; +59; −0

```diff
@@ -25,6 +25,7 @@
   getFilesystemBrowsePath,
 } from "@t3tools/client-runtime/state/filesystem";
 import {
+  type AtomCommandResult,
   isAtomCommandInterrupted,
   settlePromise,
   squashAtomCommandFailure,
@@ -47,10 +48,12 @@
 import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
 import * as Option from "effect/Option";
 import {
+  AlarmClockOffIcon,
   ArrowLeftIcon,
   ChartNoAxesColumnIcon,
   CheckIcon,
   ChevronRightIcon,
+  ClockIcon,
   CornerLeftUpIcon,
   FileSearchIcon,
   FolderGit2Icon,
@@ -86,6 +89,7 @@
 import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal";
 import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps";
 import { useHandleNewThread } from "../hooks/useHandleNewThread";
+import { useThreadActions } from "../hooks/useThreadActions";
 import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl";
 import { writeTextToClipboard } from "../hooks/useCopyToClipboard";
 import { useClientSettings } from "../hooks/useSettings";
@@ -177,6 +181,7 @@
   ITEM_ICON_CLASS,
   RECENT_THREAD_LIMIT,
   reduceCommandPaletteUiState,
+  resolveActiveThreadSnoozeAction,
   type SearchOverlayMode,
 } from "./CommandPalette.logic";
 import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic";
@@ -738,6 +743,7 @@
   const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, {
     reportFailure: false,
   });
+  const { snoozeThread, unsnoozeThread } = useThreadActions();
   const { environments } = useEnvironments();
   const desktopLocalBootstraps = useDesktopLocalBootstraps();
   const primaryEnvironmentId = usePrimaryEnvironmentId();
@@ -759,6 +765,10 @@
   const activeThreadServerConfig = useServerConfigs().get(
     activeThread?.environmentId ?? ("" as EnvironmentId),
   );
+  const canOperateActiveThread = useEnvironmentScope(
+    activeThread?.environmentId ?? null,
+    AuthOrchestrationOperateScope,
+  );
   const activeThreadReferenceCopyTarget =
     referenceThreadRef === null || (pathname === "/pull-requests" && !openPanelPullRequestUrl)
       ? null
@@ -1973,6 +1983,55 @@
     }
   }
 
+  const activeThreadSnoozeAction =
+    activeThread === null
+      ? null
+      : resolveActiveThreadSnoozeAction({
+          thread: activeThread,
+          capabilities: activeThreadServerConfig?.environment.capabilities,
+          now: new Date().toISOString(),
+        });
+  if (activeThread !== null && activeThreadSnoozeAction !== null) {
+    const threadRef = scopeThreadRef(activeThread.environmentId, activeThread.id);
+    const reportFailure = (title: string, result: AtomCommandResult<unknown, unknown>) => {
+      if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
+        toastManager.add(
+          stackedThreadToast({
+            type: "error",
+            title,
+            description: errorMessage(squashAtomCommandFailure(result)),
+          }),
+        );
+      }
+    };
+    actionItems.push(
+      activeThreadSnoozeAction.kind === "wake"
+        ? {
+            kind: "action",
+            value: "action:wake-thread",
+            searchTerms: ["wake", "unsnooze", "snooze"],
+            title: "Wake thread",
+            disabled: !canOperateActiveThread,
+            icon: <AlarmClockOffIcon className={ITEM_ICON_CLASS} />,
+            run: async () => {
+              reportFailure("Failed to wake thread", await unsnoozeThread(threadRef));
+            },
+          }
+        : {
+            kind: "action",
+            value: "action:snooze-until-attention",
+            searchTerms: ["snooze", "pull request", "pr", "review", "attention", "wait"],
+            title: "Snooze until it needs attention",
+            description: "Wakes when its pull request needs attention",
+            disabled: !canOperateActiveThread || activeThreadSnoozeAction.disabled,
+            icon: <ClockIcon className={ITEM_ICON_CLASS} />,
+            run: async () => {
+              reportFailure("Failed to snooze thread", await snoozeThread(threadRef, null));
+            },
+          },
+    );
+  }
+
   if (activeThread !== null) {
     const thread = activeThread;
     actionItems.push({
```

## apps/web/src/components/Sidebar.logic.test.ts

Status: modified; +61; −0

```diff
@@ -7,6 +7,7 @@
 import { AsyncResult } from "effect/reactivity";
 import {
   animateSidebarLayoutChanges,
+  applySidebarThreadDrop,
   archiveSelectedThreadEntries,
   buildBulkTitleRegenerationContextMenuItem,
   buildBulkUnpinContextMenuItem,
@@ -52,8 +53,10 @@
   sortProjectsForSidebar,
   sortScopedProjectsForSidebar,
   sortSidebarV2ProjectGroups,
+  sortSnoozedThreads,
   shouldCreateNewThreadInCurrentProject,
   shouldNavigateAfterThreadPark,
+  threadHoldsSnooze,
   THREAD_JUMP_HINT_SHOW_DELAY_MS,
   type SidebarListItem,
   type SidebarListMarker,
@@ -2072,6 +2075,64 @@
   );
 });
 
+describe("snoozed until a pull request needs attention", () => {
+  const timed = (id: string, snoozedUntil: string) => ({
+    id,
+    snoozedAt: "2026-09-12T08:00:00.000Z",
+    snoozedUntil,
+    snoozedUntilNeedsAttention: false,
+  });
+  const waiting = (id: string, snoozedAt: string) => ({
+    id,
+    snoozedAt,
+    snoozedUntil: null,
+    snoozedUntilNeedsAttention: true,
+  });
+
+  it("sorts after timed snoozes, oldest snooze first", () => {
+    const sorted = sortSnoozedThreads([
+      waiting("newer-wait", "2026-09-12T09:00:00.000Z"),
+      timed("later", "2026-09-14T09:00:00.000Z"),
+      waiting("older-wait", "2026-09-11T09:00:00.000Z"),
+      timed("sooner", "2026-09-13T09:00:00.000Z"),
+    ]);
+    expect(sorted.map((thread) => thread.id)).toEqual([
+      "sooner",
+      "later",
+      "older-wait",
+      "newer-wait",
+    ]);
+  });
+
+  it("is held until the server clears the flag", () => {
+    expect(threadHoldsSnooze(waiting("thread", "2026-09-12T09:00:00.000Z"))).toBe(true);
+    expect(threadHoldsSnooze({ snoozedUntil: null, snoozedUntilNeedsAttention: false })).toBe(
+      false,
+    );
+  });
+
+  it.each(["pinned", "active", "settled"] as const)("wakes when dropped into %s", (section) => {
+    const projected = applySidebarThreadDrop(
+      {
+        ...waiting("thread", "2026-09-12T09:00:00.000Z"),
+        pinnedAt: null,
+        pinOrderKey: null,
+        activeOrderKey: null,
+        settledAt: null,
+        settledOverride: null,
+        unsettledAt: null,
+      },
+      section,
+      "2026-09-12T10:00:00.000Z",
+    );
+    expect(projected).toMatchObject({
+      snoozedAt: null,
+      snoozedUntil: null,
+      snoozedUntilNeedsAttention: false,
+    });
+  });
+});
+
 describe("unseen completion with background work", () => {
   it.each([
     { kind: "command", status: "ready", topStatus: "done", receded: false, pill: "Completed" },
```

## apps/web/src/components/Sidebar.logic.ts

Status: modified; +32; −1

```diff
@@ -430,13 +430,19 @@
     | "activeOrderKey"
     | "snoozedAt"
     | "snoozedUntil"
+    | "snoozedUntilNeedsAttention"
     | "settledAt"
     | "settledOverride"
     | "unsettledAt"
   >,
 >(thread: T, section: "pinned" | "active" | "settled", now: string, orderKey?: string): T {
   const wasSettled = thread.settledOverride === "settled";
+  const awake = {
+    ...thread,
+    snoozedAt: null,
+    snoozedUntil: null,
-  const awake = { ...thread, snoozedAt: null, snoozedUntil: null };
+    snoozedUntilNeedsAttention: false,
+  };
   if (section === "settled") {
     return {
       ...awake,
@@ -1038,6 +1044,31 @@
   return input.isUnread ? "done" : null;
 }
 
+/** Whether the server still holds a snooze on the thread, timed or until a pull request needs attention. */
+export function threadHoldsSnooze(
+  thread: Pick<SidebarThreadSummary, "snoozedUntil" | "snoozedUntilNeedsAttention">,
+): boolean {
+  return thread.snoozedUntil != null || thread.snoozedUntilNeedsAttention === true;
+}
+
+/**
+ * Snoozed shelf order. Soonest wake first, since "what comes back next" is the shelf's question.
+ * Threads waiting on a pull request have no wake time, so they follow, oldest snooze first: a new
+ * one joins the end instead of reshuffling the rows above it.
+ */
+export function sortSnoozedThreads<
+  T extends Pick<SidebarThreadSummary, "snoozedAt" | "snoozedUntil" | "snoozedUntilNeedsAttention">,
+>(threads: ReadonlyArray<T>): T[] {
+  return threads.toSorted((left, right) => {
+    const leftWaits = left.snoozedUntil == null && left.snoozedUntilNeedsAttention === true;
+    const rightWaits = right.snoozedUntil == null && right.snoozedUntilNeedsAttention === true;
+    if (leftWaits !== rightWaits) return leftWaits ? 1 : -1;
+    return leftWaits
+      ? firstValidTimestampMs(left.snoozedAt) - firstValidTimestampMs(right.snoozedAt)
+      : firstValidTimestampMs(left.snoozedUntil) - firstValidTimestampMs(right.snoozedUntil);
+  });
+}
+
 export function shouldShowSidebarV2Duration(status: SidebarThreadStatus): boolean {
   return status === "working";
 }
```

## apps/web/src/components/Sidebar.snooze.test.ts

Status: modified; +40; −1

```diff
@@ -1,6 +1,10 @@
 import { describe, expect, it } from "vite-plus/test";
 
+import {
+  offersSnoozeUntilAttention,
-import { resolveSnoozePresets, snoozeWakeDescription } from "./Sidebar.snooze";
+  resolveSnoozePresets,
+  snoozeWakeDescription,
+} from "./Sidebar.snooze";
 
 // Local-time constructor so preset math is timezone-stable in tests.
 function localDate(year: number, month: number, day: number, hour: number, minute = 0): Date {
@@ -94,3 +98,38 @@
     );
   });
 });
+
+describe("offersSnoozeUntilAttention", () => {
+  const thread = {
+    pullRequests: [
+      {
+        host: "github.com",
+        repository: "acme/app",
+        number: 1,
+        url: "https://github.com/acme/app/pull/1",
+        source: "agent" as const,
+        linkedAt: "2026-10-06T22:00:00.000Z",
+        snapshot: null,
+        stack: null,
+      },
+    ],
+    settledOverride: null,
+    settledAt: null,
+    lineage: { relationshipToParent: null },
+  };
+
+  it("needs a server that advertises it, not just timed snooze", () => {
+    expect(offersSnoozeUntilAttention(thread, { threadSnoozeUntilAttention: true })).toBe(true);
+    expect(offersSnoozeUntilAttention(thread, {})).toBe(false);
+    expect(offersSnoozeUntilAttention(thread, undefined)).toBe(false);
+  });
+
+  it("needs an open linked pull request", () => {
+    expect(
+      offersSnoozeUntilAttention(
+        { ...thread, pullRequests: [] },
+        { threadSnoozeUntilAttention: true },
+      ),
+    ).toBe(false);
+  });
+});
```

## apps/web/src/components/Sidebar.snooze.ts

Status: modified; +17; −0

```diff
@@ -1,5 +1,6 @@
 import type { TimestampFormat } from "@t3tools/contracts/settings";
 import {
+  canSnoozeUntilAttention,
   resolveSnoozePresets as resolveSharedSnoozePresets,
   snoozeWakeLabel,
   type SnoozePreset,
@@ -9,6 +10,22 @@
 
 export { snoozeWakeLabel, type SnoozePreset };
 
+/** A snooze menu choice: a wake time, or null to snooze until a pull request needs attention. */
+export interface SnoozeChoice {
+  readonly snoozedUntil: string | null;
+}
+
+/** The choice behind "Until it needs attention". */
+export const SNOOZE_UNTIL_ATTENTION: SnoozeChoice = { snoozedUntil: null };
+
+/** Whether snooze menus offer "Until it needs attention": the server understands it and the thread links an open pull request. */
+export function offersSnoozeUntilAttention(
+  thread: Parameters<typeof canSnoozeUntilAttention>[0],
+  capabilities: { readonly threadSnoozeUntilAttention?: boolean } | undefined,
+): boolean {
+  return capabilities?.threadSnoozeUntilAttention === true && canSnoozeUntilAttention(thread);
+}
+
 const DAY_MS = 24 * 60 * 60 * 1_000;
 
 function timeOfDayLabel(date: Date, timestampFormat: TimestampFormat): string {
```

## apps/web/src/components/Sidebar.tsx

Status: modified; +81; −23

```diff
@@ -176,6 +176,8 @@
 import {
   buildDraftActionMenuItems,
   buildThreadActionMenuItems,
+  SNOOZE_UNTIL_ATTENTION_LABEL,
+  SNOOZE_UNTIL_ATTENTION_MENU_ID,
   threadActionRequiresOperate,
 } from "./threadActionMenu.logic";
 import {
@@ -187,7 +189,6 @@
   deleteSelectedThreadEntries,
   filterSidebarProjectScopeItems,
   formatWorkingDurationLabel,
-  firstValidTimestampMs,
   hasUnseenCompletion,
   isSidebarNestedLinkClick,
   isSidebarThreadWorking,
@@ -215,8 +216,10 @@
   sortInboxThreadsByReturn,
   sortPinnedThreadsForSidebar,
   sortSidebarV2ProjectGroups,
+  sortSnoozedThreads,
   sortThreadsForSidebar,
   sortWorkingThreadsBySend,
+  threadHoldsSnooze,
   useThreadJumpHintVisibility,
   useRetainedValue,
   useSidebarRowSubscriptionLease,
@@ -246,7 +249,14 @@
   type TerminalStatusIndicator,
   useLinkedThreadPullRequest,
 } from "./ThreadStatusIndicators";
+import {
+  offersSnoozeUntilAttention,
+  resolveSnoozePresets,
+  SNOOZE_UNTIL_ATTENTION,
-import { resolveSnoozePresets, snoozeWakeLabel, type SnoozePreset } from "./Sidebar.snooze";
+  snoozeWakeLabel,
+  type SnoozeChoice,
+} from "./Sidebar.snooze";
+import { SnoozedUntilAttentionIndicator } from "./SnoozedUntilAttentionIndicator";
 import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon";
 import { ThreadSearchMatchExcerpt } from "./ThreadSearchMatch";
 import { makeWorkspaceFileDropHandlers } from "./chat/workspaceFileDrop";
@@ -572,10 +582,11 @@
 function SnoozeMenuButton(props: {
   open: boolean;
   onOpenChange: (open: boolean) => void;
-  onSnooze: (preset: Pick<SnoozePreset, "snoozedUntil">) => void;
+  onSnooze: (choice: SnoozeChoice) => void;
+  canSnoozeUntilAttention: boolean;
   timestampFormat: TimestampFormat;
 }) {
-  const { open, onOpenChange, onSnooze, timestampFormat } = props;
+  const { open, onOpenChange, onSnooze, canSnoozeUntilAttention, timestampFormat } = props;
   // Presets resolve at open time so "In 1 hour" is relative to the click,
   // not to when the row mounted.
   const presets = useMemo(
@@ -617,6 +628,19 @@
             <MenuShortcut>{preset.whenLabel}</MenuShortcut>
           </MenuItem>
         ))}
+        {canSnoozeUntilAttention ? (
+          <>
+            <MenuSeparator />
+            <MenuItem
+              onClick={(event) => {
+                event.stopPropagation();
+                onSnooze(SNOOZE_UNTIL_ATTENTION);
+              }}
+            >
+              {SNOOZE_UNTIL_ATTENTION_LABEL}
+            </MenuItem>
+          </>
+        ) : null}
         <MenuSeparator />
         <MenuItem
           onClick={async (event) => {
@@ -1110,6 +1134,8 @@
   settlementSupported: boolean;
   // Same contract for thread.snooze/unsnooze.
   snoozeSupported: boolean;
+  // Offers "Until it needs attention" (offersSnoozeUntilAttention).
+  canSnoozeUntilAttention: boolean;
   // Renders the pin glyph. Pinned cards keep the full settle/snooze quick
   // actions: settling clears the pin server-side, and snoozing hides the
   // card until wake with the pin intact underneath. The glyph is also the
@@ -1161,7 +1187,7 @@
     event: PointerEvent,
   ) => void;
   onUnsettle: (threadRef: ScopedThreadRef) => void;
+  onSnooze: (threadRef: ScopedThreadRef, choice: SnoozeChoice) => void;
-  onSnooze: (threadRef: ScopedThreadRef, preset: Pick<SnoozePreset, "snoozedUntil">) => void;
   onUnsnooze: (threadRef: ScopedThreadRef) => void;
   onUnpin: (threadRef: ScopedThreadRef) => void;
   onAcknowledgeWoke: (threadRef: ScopedThreadRef, visitedAt: string) => void;
@@ -1525,8 +1551,8 @@
     [onUnpin, threadRef],
   );
   const handleSnoozePreset = useCallback(
-    (preset: Pick<SnoozePreset, "snoozedUntil">) => {
+    (choice: SnoozeChoice) => {
-      onSnooze(threadRef, preset);
+      onSnooze(threadRef, choice);
     },
     [onSnooze, threadRef],
   );
@@ -1862,6 +1888,8 @@
                     <span className="text-xs text-info-foreground tabular-nums">
                       {props.snoozeWakeLabelText}
                     </span>
+                  ) : variantAction === "unsnooze" && thread.snoozedUntilNeedsAttention === true ? (
+                    <SnoozedUntilAttentionIndicator />
                   ) : isWoke ? (
                     // A wake can land straight in the settled tail (e.g. PR
                     // merged while snoozed); the signal must survive the trip.
@@ -2114,6 +2142,7 @@
                           open={snoozeMenuOpen}
                           onOpenChange={setSnoozeMenuOpen}
                           onSnooze={handleSnoozePreset}
+                          canSnoozeUntilAttention={props.canSnoozeUntilAttention}
                           timestampFormat={props.timestampFormat}
                         />
                       ) : null}
@@ -2795,7 +2824,12 @@
         ).push(
           optimisticDrop.clearsSnooze
             ? projected
+            : {
+                ...projected,
+                snoozedAt: thread.snoozedAt,
+                snoozedUntil: thread.snoozedUntil,
+                snoozedUntilNeedsAttention: thread.snoozedUntilNeedsAttention === true,
-            : { ...projected, snoozedAt: thread.snoozedAt, snoozedUntil: thread.snoozedUntil },
+              },
         );
       } else {
         const section = resolveSidebarThreadSection({
@@ -2843,12 +2877,7 @@
             }),
       // Newest send first; finishing and waking again do not move a row.
       workingThreads: sortWorkingThreadsBySend(working),
-      // Soonest wake first: "what comes back next" is the shelf's question.
-      snoozedThreads: snoozed.toSorted(
-        (left, right) =>
-          firstValidTimestampMs(left.snoozedUntil ?? null) -
-          firstValidTimestampMs(right.snoozedUntil ?? null),
+      snoozedThreads: sortSnoozedThreads(snoozed),
-      ),
       settledThreads: sortSettledThreads(settled),
       snoozeNow: preciseNow,
     };
@@ -2910,7 +2939,8 @@
 
   // Arm a timeout for the earliest upcoming wake so the shelf empties the
   // moment a snooze expires instead of on the next minute tick. Sorted
+  // soonest-first, so entry 0 is the boundary; a snooze waiting on a pull
+  // request has no wake time and sorts after every timed one.
-  // soonest-first, so entry 0 is the boundary.
   useEffect(() => {
     const nextWakeAtMs =
       snoozedThreads.length > 0 && snoozedThreads[0]?.snoozedUntil != null
@@ -3641,14 +3671,14 @@
       if (
         canonicalSection === optimisticDrop.section &&
         thread.pinnedAt == null &&
+        (!optimisticDrop.clearsSnooze || !threadHoldsSnooze(thread))
-        (!optimisticDrop.clearsSnooze || thread.snoozedUntil == null)
       ) {
         setOptimisticDrop(null);
       }
       return;
     }
     if (canonicalSection !== optimisticDrop.section) return;
-    if (optimisticDrop.clearsSnooze && thread.snoozedUntil != null) return;
+    if (optimisticDrop.clearsSnooze && threadHoldsSnooze(thread)) return;
     const destinationKeys = optimisticDrop.section === "pinned" ? pinnedKeys : activeKeys;
     const canonicalDestination = destinationKeys.flatMap((key) => {
       const canonical = canonicalByKey.get(key);
@@ -4147,7 +4177,7 @@
   const performSnooze = useCallback(
     async (
       threadRef: ScopedThreadRef,
-      preset: Pick<SnoozePreset, "snoozedUntil">,
+      choice: SnoozeChoice,
       opts: { coSnoozingKeys?: ReadonlySet<string> } = {},
     ) => {
       const threadKey = scopedThreadKey(threadRef);
@@ -4159,7 +4189,7 @@
         // Snoozing the open thread moves you forward, same as settle —
         // both park the thread you're done with for now.
         const navigateAfterSnooze = planForwardNavigation(threadKey, opts.coSnoozingKeys);
-        const result = await snoozeThread(threadRef, preset.snoozedUntil);
+        const result = await snoozeThread(threadRef, choice.snoozedUntil);
         if (result._tag === "Failure") {
           // Never navigate away from a thread that did not snooze.
           return isAtomCommandInterrupted(result)
@@ -4189,11 +4219,11 @@
   const attemptSnooze = useCallback(
     (
       threadRef: ScopedThreadRef,
+      choice: SnoozeChoice,
-      preset: Pick<SnoozePreset, "snoozedUntil">,
       opts: { coSnoozingKeys?: ReadonlySet<string> } = {},
     ) => {
       void (async () => {
+        const outcome = await performSnooze(threadRef, choice, opts);
-        const outcome = await performSnooze(threadRef, preset, opts);
         if (outcome.status === "failure") {
           toastManager.add(
             stackedThreadToast({
@@ -4239,6 +4269,12 @@
         (thread) =>
           serverConfigs.get(thread.environmentId)?.environment.capabilities.threadSnooze === true &&
           canSnooze(thread, { now: selectionNow.toISOString() }),
+      );
+      const canSnoozeSelectionUntilAttention = selectedThreads.every((thread) =>
+        offersSnoozeUntilAttention(
+          thread,
+          serverConfigs.get(thread.environmentId)?.environment.capabilities,
+        ),
       );
       const titleRegenerationThreads = selectedThreads.filter(
         (thread) =>
@@ -4287,6 +4323,16 @@
                         label: `${preset.label} (${preset.whenLabel})`,
                         disabled: !canOperateThreads(selectedThreads),
                       })),
+                      ...(canSnoozeSelectionUntilAttention
+                        ? [
+                            {
+                              id: SNOOZE_UNTIL_ATTENTION_MENU_ID,
+                              label: SNOOZE_UNTIL_ATTENTION_LABEL,
+                              separatorBefore: true,
+                              disabled: !canOperateThreads(selectedThreads),
+                            },
+                          ]
+                        : []),
                       {
                         id: "snooze:custom",
                         label: "Custom…",
@@ -4338,7 +4384,9 @@
         const preset =
           clicked.value === "snooze:custom"
             ? await requestCustomSnooze()
+            : clicked.value === SNOOZE_UNTIL_ATTENTION_MENU_ID
+              ? SNOOZE_UNTIL_ATTENTION
-            : snoozePresets.find((candidate) => `snooze:${candidate.id}` === clicked.value);
+              : snoozePresets.find((candidate) => `snooze:${candidate.id}` === clicked.value);
         if (preset) {
           // Post-snooze navigation must skip threads snoozing in this same
           // batch — they are all leaving the card block together.
@@ -4601,6 +4649,10 @@
               autoSettleEnabled: thread.autoSettleDisabledAt == null,
               isSnoozed,
               canSnoozeNow: canSnooze(thread, { now: new Date().toISOString() }),
+              canSnoozeUntilAttention: offersSnoozeUntilAttention(
+                thread,
+                serverConfigs.get(thread.environmentId)?.environment.capabilities,
+              ),
               isRegeneratingTitle,
               isRunning: !threadRuntimeCanArchive(thread.runtime),
               supports: {
@@ -4621,7 +4673,9 @@
           const preset =
             clicked.value === "snooze:custom"
               ? await requestCustomSnooze()
+              : clicked.value === SNOOZE_UNTIL_ATTENTION_MENU_ID
+                ? SNOOZE_UNTIL_ATTENTION
-              : snoozePresets.find((candidate) => `snooze:${candidate.id}` === clicked.value);
+                : snoozePresets.find((candidate) => `snooze:${candidate.id}` === clicked.value);
           if (preset) attemptSnooze(threadRef, preset);
           return;
         }
@@ -5258,6 +5312,10 @@
                               serverConfigs.get(thread.environmentId)?.environment.capabilities
                                 .threadSnooze === true
                             }
+                            canSnoozeUntilAttention={offersSnoozeUntilAttention(
+                              thread,
+                              serverConfigs.get(thread.environmentId)?.environment.capabilities,
+                            )}
                             pinningSupported={
                               serverConfigs.get(thread.environmentId)?.environment.capabilities
                                 .threadPinning === true
```

## apps/web/src/components/SnoozedUntilAttentionIndicator.tsx

Status: added; +14; −0

```diff
@@ -1,0 +1,14 @@
+import { EyeIcon } from "lucide-react";
+
+/** The mark a snoozed row shows in place of its wake countdown when it waits on a pull request. */
+export function SnoozedUntilAttentionIndicator() {
+  return (
+    <span
+      role="img"
+      aria-label="Until its pull request needs attention"
+      className="inline-flex text-xs text-info-foreground"
+    >
+      <EyeIcon aria-hidden className="size-3" />
+    </span>
+  );
+}
```

## apps/web/src/components/settings/SettingsPanels.restore.test.tsx

Status: modified; +1; −0

```diff
@@ -65,6 +65,7 @@
     ["persistComposerContextStrip", "Composer context"],
     ["autoResumeLimitedThreads", "Auto-resume limited threads"],
     ["snoozeLimitedThreads", "Snooze limited threads"],
+    ["snoozePullRequestsAwaitingReview", "Snooze pull requests awaiting review"],
   ] as const)("restores %s when it is the only changed setting", async (key, label) => {
     state.settings = { ...DEFAULT_UNIFIED_SETTINGS, [key]: true };
     hooks.beginRender();
```

## apps/web/src/components/settings/SettingsPanels.tsx

Status: modified; +33; −0

```diff
@@ -588,6 +588,10 @@
       ...(settings.snoozeLimitedThreads !== DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads
         ? ["Snooze limited threads"]
         : []),
+      ...(settings.snoozePullRequestsAwaitingReview !==
+      DEFAULT_UNIFIED_SETTINGS.snoozePullRequestsAwaitingReview
+        ? ["Snooze pull requests awaiting review"]
+        : []),
       ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
       ...(settings.persistComposerContextStrip !==
       DEFAULT_UNIFIED_SETTINGS.persistComposerContextStrip
@@ -708,6 +712,7 @@
       settings.sidebarAutoSettleOnMerge,
       settings.autoResumeLimitedThreads,
       settings.snoozeLimitedThreads,
+      settings.snoozePullRequestsAwaitingReview,
       settings.sidebarProjectGroupingMode,
       settings.sidebarProjectSortOrder,
       settings.sidebarWorkingShelfEnabled,
@@ -815,6 +820,7 @@
       sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge,
       autoResumeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.autoResumeLimitedThreads,
       snoozeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads,
+      snoozePullRequestsAwaitingReview: DEFAULT_UNIFIED_SETTINGS.snoozePullRequestsAwaitingReview,
       responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode,
       enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
       continueThreadsAfterServerUpdate: DEFAULT_UNIFIED_SETTINGS.continueThreadsAfterServerUpdate,
@@ -2204,6 +2210,11 @@
     connectedEnvironments.every(
       (target) => target.serverConfig?.environment.capabilities.threadRestartContinuation === true,
     );
+  const supportsSnoozeUntilAttention =
+    connectedEnvironments.length > 0 &&
+    connectedEnvironments.every(
+      (target) => target.serverConfig?.environment.capabilities.threadSnoozeUntilAttention === true,
+    );
 
   const textGenerationProviders = serverProviders.filter(
     (provider) => provider.supportsTextGeneration !== false,
@@ -2369,6 +2380,28 @@
             />
           }
         />
+        <SettingsRow
+          serverScoped
+          {...searchableSetting("snooze-pull-requests-awaiting-review")}
+          description="Let agents snooze threads whose pull requests are only waiting on someone else's review. They wake when the pull request needs attention."
+          settingKeys={["snoozePullRequestsAwaitingReview"]}
+          status={
+            !supportsSnoozeUntilAttention
+              ? "All selected connected environments must support snoozing until a pull request needs attention."
+              : undefined
+          }
+          control={
+            <ScopedSwitch
+              settingKeys={["snoozePullRequestsAwaitingReview"]}
+              checked={settings.snoozePullRequestsAwaitingReview}
+              disabled={!supportsSnoozeUntilAttention}
+              onCheckedChange={(checked) =>
+                updateSettings({ snoozePullRequestsAwaitingReview: Boolean(checked) })
+              }
+              aria-label="Snooze pull requests awaiting review"
+            />
+          }
+        />
 
         <SettingsRow
           {...searchableSetting("working-shelf")}
```

## apps/web/src/components/settings/settingsSearch.ts

Status: modified; +6; −0

```diff
@@ -303,6 +303,12 @@
     searchTerms: ["usage quota rate limit reset wake recover continue"],
   },
   {
+    id: "snooze-pull-requests-awaiting-review",
+    title: "Snooze pull requests awaiting review",
+    to: "/settings/general",
+    searchTerms: ["agent pr watch wait reviewer approval merge attention wake"],
+  },
+  {
     id: "auto-resume-limited-threads",
     title: "Auto-resume limited threads",
     to: "/settings/general",
```

## apps/web/src/components/threadActionMenu.logic.test.ts

Status: modified; +14; −0

```diff
@@ -15,6 +15,7 @@
   autoSettleEnabled: true,
   isSnoozed: false,
   canSnoozeNow: true,
+  canSnoozeUntilAttention: false,
   isRegeneratingTitle: false,
   isRunning: false,
   supports: {
@@ -172,6 +173,19 @@
     ).not.toContain("auto-settle");
   });
 
+  it("offers snoozing until it needs attention only for threads with an open pull request", () => {
+    const children = (state: ThreadActionMenuState) =>
+      buildThreadActionMenuItems(state)
+        .find((item) => item.id === "snooze")
+        ?.children?.map((child) => child.id);
+    expect(children(baseState)).toEqual(["snooze:hour", "snooze:custom"]);
+    expect(children({ ...baseState, canSnoozeUntilAttention: true })).toEqual([
+      "snooze:hour",
+      "snooze:needs-attention",
+      "snooze:custom",
+    ]);
+  });
+
   it("disables snooze when the thread cannot snooze, keeping presets visible", () => {
     const snooze = buildThreadActionMenuItems({ ...baseState, canSnoozeNow: false }).find(
       (item) => item.id === "snooze",
```

## apps/web/src/components/threadActionMenu.logic.ts

Status: modified; +15; −0

```diff
@@ -29,6 +29,10 @@
   | "copy-thread-id"
   | "archive"
   | "delete";
+
+/** Snooze menu entry that waits on the thread's pull requests instead of a wake time. */
+export const SNOOZE_UNTIL_ATTENTION_MENU_ID = "snooze:needs-attention" as const;
+export const SNOOZE_UNTIL_ATTENTION_LABEL = "Until it needs attention";
 
 export type DraftActionMenuId =
   | "copy"
@@ -87,6 +91,8 @@
   readonly autoSettleEnabled: boolean;
   readonly isSnoozed: boolean;
   readonly canSnoozeNow: boolean;
+  /** The thread links an open pull request, so it can snooze until that needs attention. */
+  readonly canSnoozeUntilAttention: boolean;
   readonly isRegeneratingTitle: boolean;
   /** Archive rejects a thread with an attached provider, so disable it here rather than let the action fail. */
   readonly isRunning: boolean;
@@ -163,6 +169,15 @@
                     id: `snooze:${preset.id}` as const,
                     label: `${preset.label} (${preset.whenLabel})`,
                   })),
+                  ...(state.canSnoozeUntilAttention
+                    ? [
+                        {
+                          id: SNOOZE_UNTIL_ATTENTION_MENU_ID,
+                          label: SNOOZE_UNTIL_ATTENTION_LABEL,
+                          separatorBefore: true,
+                        },
+                      ]
+                    : []),
                   { id: "snooze:custom" as const, label: "Custom…", separatorBefore: true },
                 ],
               },
```

## apps/web/src/hooks/useThreadActionMenu.test.ts

Status: modified; +1; −0

```diff
@@ -51,6 +51,7 @@
   readEnvironmentSupportsPinning: () => true,
   readEnvironmentSupportsSettlement: () => true,
   readEnvironmentSupportsSnooze: () => true,
+  readEnvironmentSupportsSnoozeUntilAttention: () => true,
   readEnvironmentSupportsTitleRegeneration: () => true,
   readThreadShell: () => ({
     id: "thread",
```

## apps/web/src/hooks/useThreadActionMenu.ts

Status: modified; +15; −2

```diff
@@ -15,9 +15,14 @@
 import { useRouter } from "@tanstack/react-router";
 import { useCallback, useMemo } from "react";
 
+import {
+  offersSnoozeUntilAttention,
+  resolveSnoozePresets,
+  SNOOZE_UNTIL_ATTENTION,
+} from "../components/Sidebar.snooze";
-import { resolveSnoozePresets } from "../components/Sidebar.snooze";
 import {
   buildThreadActionMenuItems,
+  SNOOZE_UNTIL_ATTENTION_MENU_ID,
   threadActionRequiresOperate,
   type ThreadActionMenuId,
 } from "../components/threadActionMenu.logic";
@@ -30,6 +35,7 @@
   readEnvironmentSupportsPinning,
   readEnvironmentSupportsSettlement,
   readEnvironmentSupportsSnooze,
+  readEnvironmentSupportsSnoozeUntilAttention,
   readEnvironmentSupportsTitleRegeneration,
   readThreadShell,
   useProjects,
@@ -156,6 +162,11 @@
           autoSettleEnabled: thread.autoSettleDisabledAt == null,
           isSnoozed: supports.snooze && effectiveSnoozed(thread, { now: now.toISOString() }),
           canSnoozeNow: canSnooze(thread, { now: now.toISOString() }),
+          canSnoozeUntilAttention: offersSnoozeUntilAttention(thread, {
+            threadSnoozeUntilAttention: readEnvironmentSupportsSnoozeUntilAttention(
+              threadRef.environmentId,
+            ),
+          }),
           isRegeneratingTitle,
           isRunning: !threadRuntimeCanArchive(thread.runtime),
           supports,
@@ -178,7 +189,9 @@
           const preset =
             action === "snooze:custom"
               ? await requestCustomSnooze()
-              : snoozePresets.find((candidate) => `snooze:${candidate.id}` === action);
+              : action === SNOOZE_UNTIL_ATTENTION_MENU_ID
+                ? SNOOZE_UNTIL_ATTENTION
+                : snoozePresets.find((candidate) => `snooze:${candidate.id}` === action);
           if (!preset) return;
           const result = await snoozeThread(threadRef, preset.snoozedUntil);
           if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
```

## apps/web/src/hooks/useThreadActions.permissions.test.ts

Status: modified; +6; −0

```diff
@@ -87,6 +87,7 @@
       "unpin",
       "reorderPin",
       "snooze",
+      "snoozeUntilAttention",
       "unsnooze",
       "stopSession",
     ].map((action) => [action, action]),
@@ -100,6 +101,7 @@
   readEnvironmentSupportsPinReorder: () => true,
   readEnvironmentSupportsSettlement: () => true,
   readEnvironmentSupportsSnooze: () => true,
+  readEnvironmentSupportsSnoozeUntilAttention: () => true,
   readThreadShell: (ref: ScopedThreadRef) =>
     state.threads.find(
       (thread) => thread.environmentId === ref.environmentId && thread.id === ref.threadId,
@@ -194,6 +196,10 @@
       actions.snoozeThread(ref, "2099-01-01T00:00:00Z"),
   },
   {
+    name: "snoozeUntilAttention",
+    run: (actions: ThreadActions, ref: ScopedThreadRef) => actions.snoozeThread(ref, null),
+  },
+  {
     name: "unsnooze",
     run: (actions: ThreadActions, ref: ScopedThreadRef) => actions.unsnoozeThread(ref),
   },
```

[Next page](https://git.cafe/flambo/t3code/pull/7/diffs?after=ImFwcHMvd2ViL3NyYy9ob29rcy91c2VUaHJlYWRBY3Rpb25zLnBlcm1pc3Npb25zLnRlc3QudHMi&expectedVersion=1&format=markdown)
