- {/* Expand affordance / source icon */}
- {inline ? (
-
-
-
-
- {meta.label}
+ {/* Clickable header region: toggles expand for inline rows (plan §2/§5). */}
+
+ {/* Expand affordance / source icon */}
+ {expandable ? (
+
+ {expanded ? : }
- {item.relatedIssue?.identifier && (
-
e.stopPropagation()}
- >
- {item.relatedIssue.identifier}
-
- )}
- {isHidden && snoozedUntil ? (
-
-
- Reappears {reappearLabel(snoozedUntil)}
-
- ) : (
-
-
- {relativeTime(item.activityAt)}
-
- )}
-
+ ) : (
+
+
+
+ )}
-
-
- {href ? (
-
+
+
+
+ {meta.label}
+
+ {sevBadge && (
+
- {item.subject.title ?? meta.label}
+ {sevBadge.label}
+
+ )}
+ {item.relatedIssue?.identifier && (
+
e.stopPropagation()}
+ >
+ {item.relatedIssue.identifier}
+ )}
+ {isHidden && snoozedUntil ? (
+
+
+ Reappears {reappearLabel(snoozedUntil)}
+
) : (
-
- {item.subject.title ?? meta.label}
+
+
+ {relativeTime(item.activityAt)}
)}
- {item.whyNow}
- {/* Deep-link / open for non-inline rows */}
- {!inline && href && (
-
- )}
+
+
+
+ {item.subject.title ?? meta.label}
+
+
{detailLine}
- {isHidden ? (
- /* Curtain rows: restore back into the live queue. */
- onRestore && (
-
- )
- ) : (
- /* Active rows: snooze presets + dismiss. */
-
-
+ {(item.project || workspaceLabel) && (
+
+ {item.project && (
+ {
+ e.stopPropagation();
+ onFilterProject(item.project!);
+ }
+ : undefined
+ }
+ />
+ )}
+ {workspaceLabel && }
+
+ )}
+
+
+ {images.length > 0 &&
}
+
+
+ {/* Inline decision verbs on collapsed inline rows (plan §3). */}
+ {expandable && !expanded && verbs.length > 0 && (
+
+ {verbs.map((verb) => (
-
-
- {onSnooze && (
-
-
-
- Snooze
-
-
- {SNOOZE_PRESETS.map((preset) => (
- onSnooze(item, new Date(Date.now() + preset.ms).toISOString())}
- >
- {preset.label}
-
- ))}
-
-
- )}
- onDismiss(item)}>
-
- Dismiss
-
- {href && (
- <>
-
-
- Open source
-
- >
- )}
-
-
+ ))}
+
)}
+
+ {/* Controls: kept as siblings (not inside the clickable header) so they
+ never toggle expand and stay valid interactive targets. */}
+
+ {!inline && href && (
+
+ )}
+
+ {isHidden ? (
+ onRestore && (
+
+ )
+ ) : (
+
+
+
+
+
+ {onSnooze && onSnooze(item, iso)} />}
+ onDismiss(item)}>
+
+ Dismiss
+
+ {href && (
+ <>
+
+
+ Open source
+
+ >
+ )}
+
+
+ )}
+
{inline && expanded && (
@@ -247,6 +324,110 @@ export function AttentionQueueRow({
);
}
+/** Small pill used for the project / workspace chips. Clickable when `onClick`. */
+function Chip({
+ icon: Icon,
+ label,
+ onClick,
+}: {
+ icon: typeof Folder;
+ label: string;
+ onClick?: (e: MouseEvent) => void;
+}) {
+ const className = cn(
+ "inline-flex max-w-[12rem] items-center gap-1 rounded-sm border border-border/70 bg-muted/40 px-1.5 py-0.5 text-(length:--text-nano) text-muted-foreground",
+ onClick && "transition-colors hover:border-border hover:bg-accent hover:text-foreground",
+ );
+ const content = (
+ <>
+
+
{label}
+ >
+ );
+ if (onClick) {
+ return (
+
+ );
+ }
+ return
{content};
+}
+
+/** Square screenshot thumbnails at the right of the description (plan §10). */
+function ThumbnailStack({ images }: { images: AttentionDetailImage[] }) {
+ const visible = images.slice(0, 3);
+ const extra = images.length - visible.length;
+ return (
+
+
+ {visible.map((img, i) => (
+
})
+ ))}
+
+ {extra > 0 && (
+
+ +{extra}
+
+ )}
+
+ );
+}
+
+/** Snooze submenu: presets + a custom date-time (plan §6). */
+function SnoozeSubmenu({ onSnooze }: { onSnooze: (snoozedUntil: string) => void }) {
+ const [customValue, setCustomValue] = useState("");
+ const applyCustom = () => {
+ if (!customValue) return;
+ const ts = new Date(customValue);
+ if (Number.isNaN(ts.getTime())) return;
+ onSnooze(ts.toISOString());
+ };
+ return (
+
+
+
+ Snooze
+
+
+ {SNOOZE_PRESETS.map((preset) => (
+ onSnooze(preset.resolve())}>
+ {preset.label}
+
+ ))}
+
+ {/* Custom picker: a non-menu-item region so interacting with the input
+ doesn't close the menu (guard keydown/select against Radix typeahead). */}
+ e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
+ >
+
+ Custom
+
+ setCustomValue(e.target.value)}
+ className="w-full rounded-sm border border-border bg-background px-2 py-1 text-xs"
+ />
+
+
+
+
+ );
+}
+
/** Compact "when does this snooze end" label, e.g. `in 2h`, `in 3d`. */
function reappearLabel(snoozedUntil: string): string {
const diffMs = new Date(snoozedUntil).getTime() - Date.now();
diff --git a/ui/src/components/ToastViewport.tsx b/ui/src/components/ToastViewport.tsx
index 3abdc20b13e..23a53bda246 100644
--- a/ui/src/components/ToastViewport.tsx
+++ b/ui/src/components/ToastViewport.tsx
@@ -56,15 +56,27 @@ function AnimatedToast({
{toast.body}
)}
- {toast.action && (
-
onDismiss(toast.id)}
- className="mt-2 inline-flex text-xs font-medium underline underline-offset-4 hover:opacity-90"
- >
- {toast.action.label}
-
- )}
+ {toast.action &&
+ (toast.action.onClick ? (
+
+ ) : toast.action.href ? (
+
onDismiss(toast.id)}
+ className="mt-2 inline-flex text-xs font-medium underline underline-offset-4 hover:opacity-90"
+ >
+ {toast.action.label}
+
+ ) : null)}