| ... | ... | @@ -2182,6 +2182,40 @@ export default function SidebarV2() { |
| 2182 | 2182 | ); |
| 2183 | 2183 | // One snooze per thread at a time — same double-dispatch guard as settle. |
| 2184 | 2184 | const snoozingThreadKeysRef = useRef(new Set<string>()); |
| 2185 | const performSnooze = useCallback( |
| 2186 | async ( |
| 2187 | threadRef: ScopedThreadRef, |
| 2188 | preset: SnoozePreset, |
| 2189 | opts: { coSnoozingKeys?: ReadonlySet<string> } = {}, |
| 2190 | ) => { |
| 2191 | const threadKey = scopedThreadKey(threadRef); |
| 2192 | if (snoozingThreadKeysRef.current.has(threadKey)) { |
| 2193 | return { status: "skipped" } as const; |
| 2194 | } |
| 2195 | snoozingThreadKeysRef.current.add(threadKey); |
| 2196 | try { |
| 2197 | // Snoozing the open thread moves you forward, same as settle — |
| 2198 | // both park the thread you're done with for now. |
| 2199 | const navigateAfterSnooze = planForwardNavigation(threadKey, opts.coSnoozingKeys); |
| 2200 | const result = await snoozeThread(threadRef, preset.snoozedUntil); |
| 2201 | if (result._tag === "Failure") { |
| 2202 | // Never navigate away from a thread that did not snooze. |
| 2203 | return isAtomCommandInterrupted(result) |
| 2204 | ? ({ status: "interrupted" } as const) |
| 2205 | : ({ status: "failure", error: squashAtomCommandFailure(result) } as const); |
| 2206 | } |
| 2207 | // Only move forward if the user is still on the snoozed thread — |
| 2208 | // a navigation made during the await wins over ours. |
| 2209 | if (routeThreadKeyRef.current === threadKey) { |
| 2210 | navigateAfterSnooze?.(); |
| 2211 | } |
| 2212 | return { status: "success" } as const; |
| 2213 | } finally { |
| 2214 | snoozingThreadKeysRef.current.delete(threadKey); |
| 2215 | } |
| 2216 | }, |
| 2217 | [planForwardNavigation, snoozeThread], |
| 2218 | ); |
| 2185 | 2219 | const attemptSnooze = useCallback( |
| 2186 | 2220 | ( |
| 2187 | 2221 | threadRef: ScopedThreadRef, |
| ... | ... | @@ -2189,52 +2223,35 @@ export default function SidebarV2() { |
| 2189 | 2223 | opts: { coSnoozingKeys?: ReadonlySet<string> } = {}, |
| 2190 | 2224 | ) => { |
| 2191 | 2225 | void (async () => { |
| 2192 | | const threadKey = scopedThreadKey(threadRef); |
| 2193 | | if (snoozingThreadKeysRef.current.has(threadKey)) return; |
| 2194 | | snoozingThreadKeysRef.current.add(threadKey); |
| 2195 | | try { |
| 2196 | | // Snoozing the open thread moves you forward, same as settle — |
| 2197 | | // both park the thread you're done with for now. |
| 2198 | | const navigateAfterSnooze = planForwardNavigation(threadKey, opts.coSnoozingKeys); |
| 2199 | | const result = await snoozeThread(threadRef, preset.snoozedUntil); |
| 2200 | | if (result._tag === "Failure") { |
| 2201 | | // Never navigate away from a thread that did not snooze. |
| 2202 | | if (!isAtomCommandInterrupted(result)) { |
| 2203 | | const error = squashAtomCommandFailure(result); |
| 2204 | | toastManager.add( |
| 2205 | | stackedThreadToast({ |
| 2206 | | type: "error", |
| 2207 | | title: "Failed to snooze thread", |
| 2208 | | description: error instanceof Error ? error.message : "An error occurred.", |
| 2209 | | }), |
| 2210 | | ); |
| 2211 | | } |
| 2212 | | return; |
| 2213 | | } |
| 2214 | | // Snooze hides the row, so the toast is the only confirmation — |
| 2215 | | // and the Undo is the escape hatch for a mis-click. |
| 2226 | const outcome = await performSnooze(threadRef, preset, opts); |
| 2227 | if (outcome.status === "failure") { |
| 2216 | 2228 | toastManager.add( |
| 2217 | 2229 | stackedThreadToast({ |
| 2218 | | type: "success", |
| 2219 | | title: `Snoozed until ${snoozeWakeDescription(preset.snoozedUntil, new Date(), timestampFormat)}`, |
| 2220 | | timeout: 5_000, |
| 2221 | | actionProps: { |
| 2222 | | children: "Undo", |
| 2223 | | onClick: () => attemptUnsnooze(threadRef), |
| 2224 | | }, |
| 2230 | type: "error", |
| 2231 | title: "Failed to snooze thread", |
| 2232 | description: |
| 2233 | outcome.error instanceof Error ? outcome.error.message : "An error occurred.", |
| 2225 | 2234 | }), |
| 2226 | 2235 | ); |
| 2227 | | // Only move forward if the user is still on the snoozed thread — |
| 2228 | | // a navigation made during the await wins over ours. |
| 2229 | | if (routeThreadKeyRef.current === threadKey) { |
| 2230 | | navigateAfterSnooze?.(); |
| 2231 | | } |
| 2232 | | } finally { |
| 2233 | | snoozingThreadKeysRef.current.delete(threadKey); |
| 2236 | return; |
| 2234 | 2237 | } |
| 2238 | if (outcome.status !== "success") return; |
| 2239 | // Snooze hides the row, so the toast is the only confirmation — |
| 2240 | // and the Undo is the escape hatch for a mis-click. |
| 2241 | toastManager.add( |
| 2242 | stackedThreadToast({ |
| 2243 | type: "success", |
| 2244 | title: `Snoozed until ${snoozeWakeDescription(preset.snoozedUntil, new Date(), timestampFormat)}`, |
| 2245 | timeout: 5_000, |
| 2246 | actionProps: { |
| 2247 | children: "Undo", |
| 2248 | onClick: () => attemptUnsnooze(threadRef), |
| 2249 | }, |
| 2250 | }), |
| 2251 | ); |
| 2235 | 2252 | })(); |
| 2236 | 2253 | }, |
| 2237 | | [attemptUnsnooze, planForwardNavigation, snoozeThread, timestampFormat], |
| 2254 | [attemptUnsnooze, performSnooze, timestampFormat], |
| 2238 | 2255 | ); |
| 2239 | 2256 | |
| 2240 | 2257 | const removeFromSelection = useThreadSelectionStore((s) => s.removeFromSelection); |
| ... | ... | @@ -2308,12 +2325,55 @@ export default function SidebarV2() { |
| 2308 | 2325 | // Post-snooze navigation must skip threads snoozing in this same |
| 2309 | 2326 | // batch — they are all leaving the card block together. |
| 2310 | 2327 | const coSnoozingKeys = new Set(threadKeys); |
| 2311 | | for (const thread of selectedThreads) { |
| 2312 | | attemptSnooze(scopeThreadRef(thread.environmentId, thread.id), preset, { |
| 2313 | | coSnoozingKeys, |
| 2314 | | }); |
| 2315 | | } |
| 2316 | 2328 | clearSelection(); |
| 2329 | const outcomes = await Promise.all( |
| 2330 | selectedThreads.map(async (thread) => { |
| 2331 | const threadRef = scopeThreadRef(thread.environmentId, thread.id); |
| 2332 | const outcome = await performSnooze(threadRef, preset, { coSnoozingKeys }); |
| 2333 | return { outcome, threadRef }; |
| 2334 | }), |
| 2335 | ); |
| 2336 | const snoozedThreadRefs = outcomes.flatMap(({ outcome, threadRef }) => |
| 2337 | outcome.status === "success" ? [threadRef] : [], |
| 2338 | ); |
| 2339 | const failures = outcomes.flatMap(({ outcome }) => |
| 2340 | outcome.status === "failure" ? [outcome.error] : [], |
| 2341 | ); |
| 2342 | |
| 2343 | if (snoozedThreadRefs.length > 0) { |
| 2344 | const snoozedCount = snoozedThreadRefs.length; |
| 2345 | const failedCount = failures.length; |
| 2346 | toastManager.add( |
| 2347 | stackedThreadToast({ |
| 2348 | type: failedCount > 0 ? "warning" : "success", |
| 2349 | title: |
| 2350 | failedCount > 0 |
| 2351 | ? `Snoozed ${snoozedCount} of ${selectedThreads.length} threads` |
| 2352 | : `Snoozed ${snoozedCount} thread${snoozedCount === 1 ? "" : "s"}`, |
| 2353 | description: |
| 2354 | failedCount > 0 |
| 2355 | ? `${failedCount} thread${failedCount === 1 ? "" : "s"} couldn't be snoozed.` |
| 2356 | : undefined, |
| 2357 | timeout: 5_000, |
| 2358 | actionProps: { |
| 2359 | children: "Undo", |
| 2360 | onClick: () => { |
| 2361 | for (const threadRef of snoozedThreadRefs) attemptUnsnooze(threadRef); |
| 2362 | }, |
| 2363 | }, |
| 2364 | }), |
| 2365 | ); |
| 2366 | } else if (failures.length > 0) { |
| 2367 | const firstError = failures[0]; |
| 2368 | toastManager.add( |
| 2369 | stackedThreadToast({ |
| 2370 | type: "error", |
| 2371 | title: "Failed to snooze threads", |
| 2372 | description: |
| 2373 | firstError instanceof Error ? firstError.message : "An error occurred.", |
| 2374 | }), |
| 2375 | ); |
| 2376 | } |
| 2317 | 2377 | } |
| 2318 | 2378 | return; |
| 2319 | 2379 | } |
| ... | ... | @@ -2409,8 +2469,10 @@ export default function SidebarV2() { |
| 2409 | 2469 | confirmThreadDelete, |
| 2410 | 2470 | deleteThread, |
| 2411 | 2471 | markThreadUnread, |
| 2472 | performSnooze, |
| 2412 | 2473 | removeFromSelection, |
| 2413 | 2474 | serverConfigs, |
| 2475 | attemptUnsnooze, |
| 2414 | 2476 | updateThreadMetadata, |
| 2415 | 2477 | timestampFormat, |
| 2416 | 2478 | ], |