fix(notifications): improve agent progress notifications and permission handling (#459)

* fix(notifications): dismiss cross-window permissions, retry countdown, subtask filter
This commit is contained in:
Nguyễn Ngô Thượng
2026-02-21 00:46:15 +02:00
committed by GitHub
parent 2e08501ea5
commit 62e5c3edfe
9 changed files with 125 additions and 75 deletions
@@ -1875,6 +1875,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
isWaitingForPermission={working.isWaitingForPermission}
wasAborted={working.wasAborted}
abortActive={working.abortActive}
retryInfo={working.retryInfo}
showAbortStatus={showAbortStatus}
/>
</div>
@@ -53,6 +53,7 @@ interface StatusRowProps {
isWaitingForPermission?: boolean;
wasAborted?: boolean;
abortActive?: boolean;
retryInfo?: { attempt?: number; next?: number } | null;
// Abort state (for mobile/vscode)
showAbort?: boolean;
onAbort?: () => void;
@@ -67,6 +68,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
isWaitingForPermission,
wasAborted,
abortActive,
retryInfo,
showAbort,
onAbort,
showAbortStatus,
@@ -211,6 +213,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
statusText={statusText}
isGenericStatus={isGenericStatus}
isWaitingForPermission={isWaitingForPermission}
retryInfo={retryInfo}
/>
) : null}
</div>
@@ -6,6 +6,7 @@ interface WorkingPlaceholderProps {
statusText: string | null;
isGenericStatus?: boolean;
isWaitingForPermission?: boolean;
retryInfo?: { attempt?: number; next?: number } | null;
}
const STATUS_DISPLAY_TIME_MS = 1200;
@@ -15,6 +16,7 @@ export function WorkingPlaceholder({
statusText,
isGenericStatus,
isWaitingForPermission,
retryInfo,
}: WorkingPlaceholderProps) {
const [displayedText, setDisplayedText] = React.useState<string | null>(null);
const [displayedPermission, setDisplayedPermission] = React.useState<boolean>(false);
@@ -23,6 +25,35 @@ export function WorkingPlaceholder({
const queuedStatusRef = React.useRef<{ text: string; permission: boolean } | null>(null);
const processQueueTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
// Countdown state for retry mode
const retryNextRef = React.useRef<number | null>(null);
const retryStartRef = React.useRef<number | null>(null);
const [retryCountdown, setRetryCountdown] = React.useState<number | null>(null);
React.useEffect(() => {
const next = retryInfo?.next;
if (!next || next <= 0) {
retryNextRef.current = null;
retryStartRef.current = null;
setRetryCountdown(null);
return;
}
// Start a fresh countdown when next value or attempt changes
retryNextRef.current = next;
retryStartRef.current = Date.now();
const update = () => {
const elapsed = Date.now() - (retryStartRef.current ?? Date.now());
const remaining = Math.max(0, next - elapsed);
setRetryCountdown(Math.ceil(remaining / 1000));
};
update();
const id = setInterval(update, 500);
return () => clearInterval(id);
}, [retryInfo?.next, retryInfo?.attempt]);
const clearTimers = React.useCallback(() => {
if (processQueueTimerRef.current) {
clearTimeout(processQueueTimerRef.current);
@@ -61,6 +92,13 @@ export function WorkingPlaceholder({
return;
}
// Retry state has its own display — skip the normal queue
if (retryInfo) {
clearTimers();
queuedStatusRef.current = null;
return;
}
const incomingText = isWaitingForPermission ? 'waiting for permission' : statusText;
const incomingPermission = Boolean(isWaitingForPermission);
const incomingGeneric = Boolean(isGenericStatus) && !incomingPermission;
@@ -96,6 +134,7 @@ export function WorkingPlaceholder({
statusText,
isGenericStatus,
isWaitingForPermission,
retryInfo,
displayedText,
displayedPermission,
clearTimers,
@@ -105,7 +144,33 @@ export function WorkingPlaceholder({
React.useEffect(() => () => clearTimers(), [clearTimers]);
if (!isWorking || !displayedText) {
if (!isWorking) {
return null;
}
// Retry state: show countdown and attempt info
if (retryInfo) {
const attemptLabel = retryInfo.attempt && retryInfo.attempt > 1 ? ` (attempt ${retryInfo.attempt})` : '';
const countdownLabel = retryCountdown !== null && retryCountdown > 0 ? ` in ${retryCountdown}s` : '';
const retryText = `Retrying${countdownLabel}${attemptLabel}...`;
return (
<div
className="flex h-full items-center text-muted-foreground pl-[2ch]"
role="status"
aria-live="polite"
aria-label={retryText}
>
<span className="flex items-center gap-1.5">
<Text variant="shine" className="typography-ui-header">
{retryText}
</Text>
</span>
</div>
);
}
if (!displayedText) {
return null;
}
@@ -675,6 +675,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const deviceInfo = useDeviceInfo();
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks);
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
const gitDirectories = useGitStore((state) => state.directories);
@@ -1810,7 +1811,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const hasChildren = node.children.length > 0;
const isPinnedSession = pinnedSessionIds.has(session.id);
const isExpanded = expandedParents.has(session.id);
const needsAttention = sessionAttentionStates.get(session.id)?.needsAttention === true;
const isSubtaskSession = Boolean((session as Session & { parentID?: string | null }).parentID);
const rawNeedsAttention = sessionAttentionStates.get(session.id)?.needsAttention === true;
// When notifyOnSubtasks is disabled, suppress attention dots for child sessions.
const needsAttention = rawNeedsAttention && (!isSubtaskSession || notifyOnSubtasks);
const sessionSummary = session.summary as
| {
additions?: number | string | null;
@@ -2162,6 +2166,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
copiedSessionId,
mobileVariant,
openMenuSessionId,
notifyOnSubtasks,
],
);