@@ -965,13 +971,27 @@ function NoticeShelf({ notices, floating = false }: { notices: NoticeItem[]; flo
onPauseChange?.(notice.id)}
+ onMouseLeave={(event) => {
+ if (!event.currentTarget.contains(document.activeElement)) onPauseChange?.(null);
+ }}
+ onFocus={() => onPauseChange?.(notice.id)}
+ onBlur={(event) => {
+ if (!event.currentTarget.matches(":hover")) onPauseChange?.(null);
+ }}
style={{
display: "flex",
- alignItems: "center",
+ // Top-align children so the type dot sits by the first line on multi-line toasts
+ alignItems: "flex-start",
gap: 10,
minHeight: 60,
- height: 60,
- maxHeight: 60,
+ height: "auto",
+ // 整体高度上限:超出后由文本区内部滚动承担(见下方 span 的 overflowY),
+ // 容器自身保持 hidden,小圆点固定在顶部不随文本滚动
+ maxHeight: NOTICE_MAX_HEIGHT_PX,
+ // The floating wrapper is pointerEvents:"none" (click-through by design),
+ // so the toast itself must opt back into interactivity or hover events never reach it
+ pointerEvents: "auto",
marginBottom: index === notices.length - 1 ? 0 : 6,
overflow: "hidden",
borderRadius: 14,
@@ -983,12 +1003,15 @@ function NoticeShelf({ notices, floating = false }: { notices: NoticeItem[]; flo
boxShadow: floating
? "0 1px 2px rgba(15,23,42,0.05), 0 10px 28px -14px rgba(15,23,42,0.24)"
: "0 1px 2px rgba(15,23,42,0.04), 0 8px 24px -12px rgba(15,23,42,0.10)",
- fontSize: 18,
- lineHeight: 1.45,
- transformOrigin: "top center",
+ fontSize: 14,
+ lineHeight: 1.5,
+ transformOrigin: "top right",
+ // Use backwards fill for the entrance animation so height styles return to
+ // inline styles once it finishes; otherwise the keyframe's fixed 60px would
+ // stick around in fill mode and permanently clamp the expanded toast
animation: notice.exiting
? "notice-shelf-out 0.18s ease-in forwards"
- : "notice-shelf-in 0.18s ease-out both",
+ : "notice-shelf-in 0.18s ease-out backwards",
padding: "0 12px",
}}
>
@@ -999,9 +1022,18 @@ function NoticeShelf({ notices, floating = false }: { notices: NoticeItem[]; flo
borderRadius: "50%",
background: color,
flexShrink: 0,
+ // Align with the optical center of the first text line: 14px vertical
+ // padding + (21px line box - 7px dot) / 2
+ marginTop: 21,
}}
/>
-
+ {/* Full text by default: pre-line preserves \n (nowrap/normal collapse
+ newlines into spaces) and long lines wrap instead of truncating;
+ content taller than the cap scrolls inside the text area */}
+
{notice.message}
diff --git a/hooks/useAgentSession.ts b/hooks/useAgentSession.ts
index 1a5eb10a2..38533c2eb 100644
--- a/hooks/useAgentSession.ts
+++ b/hooks/useAgentSession.ts
@@ -1898,8 +1898,18 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
return () => clearTimeout(t);
}, [compactResult]);
+ // Pause notice expiry while hovered or focused.
+ // The remainingMs/startedAt/oldestId refs implement a true pause-and-resume instead of resetting the 5s timer.
+ const [pausedNoticeId, setPausedNoticeId] = useState
(null);
+ const noticeRemainingMsRef = useRef(NOTICE_VISIBLE_MS);
+ const noticeTimerStartedAtRef = useRef(null);
+ const noticeOldestIdRef = useRef(null);
+
useEffect(() => {
- if (noticeState.visible.length === 0) return;
+ if (noticeState.visible.length === 0) {
+ noticeOldestIdRef.current = null;
+ return;
+ }
const exiting = noticeState.visible.find((notice) => notice.exiting);
if (exiting) {
const t = setTimeout(() => {
@@ -1909,11 +1919,28 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
}
const oldest = noticeState.visible[0];
if (!oldest) return;
+ // Oldest visible notice changed; restart the countdown
+ if (noticeOldestIdRef.current !== oldest.id) {
+ noticeOldestIdRef.current = oldest.id;
+ noticeRemainingMsRef.current = NOTICE_VISIBLE_MS;
+ }
+ if (noticeState.visible.some((notice) => notice.id === pausedNoticeId)) return;
+ noticeTimerStartedAtRef.current = Date.now();
const t = setTimeout(() => {
dispatchNotice({ type: "mark_oldest_exiting" });
- }, NOTICE_VISIBLE_MS);
- return () => clearTimeout(t);
- }, [noticeState.visible]);
+ }, noticeRemainingMsRef.current);
+ return () => {
+ clearTimeout(t);
+ // Accrue the elapsed time so the countdown resumes from the remaining time
+ if (noticeTimerStartedAtRef.current !== null) {
+ noticeRemainingMsRef.current = Math.max(
+ 0,
+ noticeRemainingMsRef.current - (Date.now() - noticeTimerStartedAtRef.current),
+ );
+ noticeTimerStartedAtRef.current = null;
+ }
+ };
+ }, [noticeState.visible, pausedNoticeId]);
useEffect(() => {
setSessionStatsOverride(null);
@@ -1939,6 +1966,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
handleCompact, handleSteer, handleFollowUp, handlePromptWithStreamingBehavior, handleAbortCompaction,
handleRecallQueue,
handleBuiltinSlashCommand,
+ setNoticePaused: setPausedNoticeId,
handleToolPresetChange, handleThinkingLevelChange, loadTools, loadSlashCommands, setActiveLeafId, setData, setMessages,
scrollToBottom, scrollUserMsgToTop,
dispatch, setAgentRunning, setForkingEntryId,