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:
committed by
GitHub
parent
2e08501ea5
commit
62e5c3edfe
@@ -1875,6 +1875,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
isWaitingForPermission={working.isWaitingForPermission}
|
isWaitingForPermission={working.isWaitingForPermission}
|
||||||
wasAborted={working.wasAborted}
|
wasAborted={working.wasAborted}
|
||||||
abortActive={working.abortActive}
|
abortActive={working.abortActive}
|
||||||
|
retryInfo={working.retryInfo}
|
||||||
showAbortStatus={showAbortStatus}
|
showAbortStatus={showAbortStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ interface StatusRowProps {
|
|||||||
isWaitingForPermission?: boolean;
|
isWaitingForPermission?: boolean;
|
||||||
wasAborted?: boolean;
|
wasAborted?: boolean;
|
||||||
abortActive?: boolean;
|
abortActive?: boolean;
|
||||||
|
retryInfo?: { attempt?: number; next?: number } | null;
|
||||||
// Abort state (for mobile/vscode)
|
// Abort state (for mobile/vscode)
|
||||||
showAbort?: boolean;
|
showAbort?: boolean;
|
||||||
onAbort?: () => void;
|
onAbort?: () => void;
|
||||||
@@ -67,6 +68,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
|||||||
isWaitingForPermission,
|
isWaitingForPermission,
|
||||||
wasAborted,
|
wasAborted,
|
||||||
abortActive,
|
abortActive,
|
||||||
|
retryInfo,
|
||||||
showAbort,
|
showAbort,
|
||||||
onAbort,
|
onAbort,
|
||||||
showAbortStatus,
|
showAbortStatus,
|
||||||
@@ -211,6 +213,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
|||||||
statusText={statusText}
|
statusText={statusText}
|
||||||
isGenericStatus={isGenericStatus}
|
isGenericStatus={isGenericStatus}
|
||||||
isWaitingForPermission={isWaitingForPermission}
|
isWaitingForPermission={isWaitingForPermission}
|
||||||
|
retryInfo={retryInfo}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface WorkingPlaceholderProps {
|
|||||||
statusText: string | null;
|
statusText: string | null;
|
||||||
isGenericStatus?: boolean;
|
isGenericStatus?: boolean;
|
||||||
isWaitingForPermission?: boolean;
|
isWaitingForPermission?: boolean;
|
||||||
|
retryInfo?: { attempt?: number; next?: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_DISPLAY_TIME_MS = 1200;
|
const STATUS_DISPLAY_TIME_MS = 1200;
|
||||||
@@ -15,6 +16,7 @@ export function WorkingPlaceholder({
|
|||||||
statusText,
|
statusText,
|
||||||
isGenericStatus,
|
isGenericStatus,
|
||||||
isWaitingForPermission,
|
isWaitingForPermission,
|
||||||
|
retryInfo,
|
||||||
}: WorkingPlaceholderProps) {
|
}: WorkingPlaceholderProps) {
|
||||||
const [displayedText, setDisplayedText] = React.useState<string | null>(null);
|
const [displayedText, setDisplayedText] = React.useState<string | null>(null);
|
||||||
const [displayedPermission, setDisplayedPermission] = React.useState<boolean>(false);
|
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 queuedStatusRef = React.useRef<{ text: string; permission: boolean } | null>(null);
|
||||||
const processQueueTimerRef = React.useRef<ReturnType<typeof setTimeout> | 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(() => {
|
const clearTimers = React.useCallback(() => {
|
||||||
if (processQueueTimerRef.current) {
|
if (processQueueTimerRef.current) {
|
||||||
clearTimeout(processQueueTimerRef.current);
|
clearTimeout(processQueueTimerRef.current);
|
||||||
@@ -61,6 +92,13 @@ export function WorkingPlaceholder({
|
|||||||
return;
|
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 incomingText = isWaitingForPermission ? 'waiting for permission' : statusText;
|
||||||
const incomingPermission = Boolean(isWaitingForPermission);
|
const incomingPermission = Boolean(isWaitingForPermission);
|
||||||
const incomingGeneric = Boolean(isGenericStatus) && !incomingPermission;
|
const incomingGeneric = Boolean(isGenericStatus) && !incomingPermission;
|
||||||
@@ -96,6 +134,7 @@ export function WorkingPlaceholder({
|
|||||||
statusText,
|
statusText,
|
||||||
isGenericStatus,
|
isGenericStatus,
|
||||||
isWaitingForPermission,
|
isWaitingForPermission,
|
||||||
|
retryInfo,
|
||||||
displayedText,
|
displayedText,
|
||||||
displayedPermission,
|
displayedPermission,
|
||||||
clearTimers,
|
clearTimers,
|
||||||
@@ -105,7 +144,33 @@ export function WorkingPlaceholder({
|
|||||||
|
|
||||||
React.useEffect(() => () => clearTimers(), [clearTimers]);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -675,6 +675,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const deviceInfo = useDeviceInfo();
|
const deviceInfo = useDeviceInfo();
|
||||||
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
||||||
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
|
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
|
||||||
|
const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks);
|
||||||
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
|
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
|
||||||
|
|
||||||
const gitDirectories = useGitStore((state) => state.directories);
|
const gitDirectories = useGitStore((state) => state.directories);
|
||||||
@@ -1810,7 +1811,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const hasChildren = node.children.length > 0;
|
const hasChildren = node.children.length > 0;
|
||||||
const isPinnedSession = pinnedSessionIds.has(session.id);
|
const isPinnedSession = pinnedSessionIds.has(session.id);
|
||||||
const isExpanded = expandedParents.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
|
const sessionSummary = session.summary as
|
||||||
| {
|
| {
|
||||||
additions?: number | string | null;
|
additions?: number | string | null;
|
||||||
@@ -2162,6 +2166,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
copiedSessionId,
|
copiedSessionId,
|
||||||
mobileVariant,
|
mobileVariant,
|
||||||
openMenuSessionId,
|
openMenuSessionId,
|
||||||
|
notifyOnSubtasks,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface WorkingSummary {
|
|||||||
abortActive: boolean;
|
abortActive: boolean;
|
||||||
lastCompletionId: string | null;
|
lastCompletionId: string | null;
|
||||||
isComplete: boolean;
|
isComplete: boolean;
|
||||||
|
retryInfo: { attempt?: number; next?: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormingSummary {
|
interface FormingSummary {
|
||||||
@@ -70,6 +71,7 @@ const DEFAULT_WORKING: WorkingSummary = {
|
|||||||
abortActive: false,
|
abortActive: false,
|
||||||
lastCompletionId: null,
|
lastCompletionId: null,
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
|
retryInfo: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAssistantMessage = (message: Message): message is AssistantMessageWithState => message.role === 'assistant';
|
const isAssistantMessage = (message: Message): message is AssistantMessageWithState => message.role === 'assistant';
|
||||||
@@ -123,6 +125,18 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
|||||||
|
|
||||||
const { phase: activityPhase, isWorking: isPhaseWorking } = useCurrentSessionActivity();
|
const { phase: activityPhase, isWorking: isPhaseWorking } = useCurrentSessionActivity();
|
||||||
|
|
||||||
|
const sessionRetryAttempt = useSessionStore((state) => {
|
||||||
|
if (!currentSessionId || !state.sessionStatus) return undefined;
|
||||||
|
const s = state.sessionStatus.get(currentSessionId);
|
||||||
|
return s?.type === 'retry' ? s.attempt : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionRetryNext = useSessionStore((state) => {
|
||||||
|
if (!currentSessionId || !state.sessionStatus) return undefined;
|
||||||
|
const s = state.sessionStatus.get(currentSessionId);
|
||||||
|
return s?.type === 'retry' ? s.next : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
const sessionMessages = React.useMemo<Array<{ info: Message; parts: Part[] }>>(() => {
|
const sessionMessages = React.useMemo<Array<{ info: Message; parts: Part[] }>>(() => {
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
return [];
|
return [];
|
||||||
@@ -293,12 +307,14 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
|||||||
isCooldown: false,
|
isCooldown: false,
|
||||||
statusText: null,
|
statusText: null,
|
||||||
canAbort: false,
|
canAbort: false,
|
||||||
|
retryInfo: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWorking = isPhaseWorking;
|
const isWorking = isPhaseWorking;
|
||||||
const isStreaming = activityPhase === 'busy';
|
const isStreaming = activityPhase === 'busy';
|
||||||
const isCooldown = false;
|
const isCooldown = false;
|
||||||
|
const isRetry = activityPhase === 'retry';
|
||||||
|
|
||||||
let activity: AssistantActivity = 'idle';
|
let activity: AssistantActivity = 'idle';
|
||||||
if (isWorking) {
|
if (isWorking) {
|
||||||
@@ -309,6 +325,10 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const retryInfo = isRetry
|
||||||
|
? { attempt: sessionRetryAttempt, next: sessionRetryNext }
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activity,
|
activity,
|
||||||
hasWorkingContext: isWorking,
|
hasWorkingContext: isWorking,
|
||||||
@@ -328,8 +348,9 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
|||||||
abortActive: false,
|
abortActive: false,
|
||||||
lastCompletionId: null,
|
lastCompletionId: null,
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
|
retryInfo,
|
||||||
};
|
};
|
||||||
}, [activityPhase, isPhaseWorking, parsedStatus, abortState]);
|
}, [activityPhase, isPhaseWorking, parsedStatus, abortState, sessionRetryAttempt, sessionRetryNext]);
|
||||||
|
|
||||||
const forming = React.useMemo<FormingSummary>(() => {
|
const forming = React.useMemo<FormingSummary>(() => {
|
||||||
|
|
||||||
@@ -380,6 +401,7 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
|
|||||||
statusText: 'waiting for permission',
|
statusText: 'waiting for permission',
|
||||||
isWaitingForPermission: true,
|
isWaitingForPermission: true,
|
||||||
canAbort: false,
|
canAbort: false,
|
||||||
|
retryInfo: null,
|
||||||
};
|
};
|
||||||
}, [currentSessionId, permissions, baseWorking]);
|
}, [currentSessionId, permissions, baseWorking]);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENABLE_EMPTY_RESPONSE_DETECTION = false;
|
|
||||||
const TEXT_SHRINK_TOLERANCE = 50;
|
const TEXT_SHRINK_TOLERANCE = 50;
|
||||||
const RESYNC_DEBOUNCE_MS = 750;
|
const RESYNC_DEBOUNCE_MS = 750;
|
||||||
const QUESTION_RECONCILE_COOLDOWN_MS = 1500;
|
const QUESTION_RECONCILE_COOLDOWN_MS = 1500;
|
||||||
@@ -109,6 +108,7 @@ export const useEventStream = () => {
|
|||||||
updateMessageInfo,
|
updateMessageInfo,
|
||||||
updateSessionCompaction,
|
updateSessionCompaction,
|
||||||
addPermission,
|
addPermission,
|
||||||
|
dismissPermission,
|
||||||
addQuestion,
|
addQuestion,
|
||||||
dismissQuestion,
|
dismissQuestion,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
@@ -357,7 +357,6 @@ export const useEventStream = () => {
|
|||||||
const unsubscribeRef = React.useRef<(() => void) | null>(null);
|
const unsubscribeRef = React.useRef<(() => void) | null>(null);
|
||||||
const reconnectTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
const reconnectAttemptsRef = React.useRef(0);
|
const reconnectAttemptsRef = React.useRef(0);
|
||||||
const emptyResponseToastShownRef = React.useRef<Set<string>>(new Set());
|
|
||||||
const missingMessageHydrationRef = React.useRef<Set<string>>(new Set());
|
const missingMessageHydrationRef = React.useRef<Set<string>>(new Set());
|
||||||
const metadataRefreshTimestampsRef = React.useRef<Map<string, number>>(new Map());
|
const metadataRefreshTimestampsRef = React.useRef<Map<string, number>>(new Map());
|
||||||
const sessionRefreshTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
const sessionRefreshTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -1338,75 +1337,6 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
void saveSessionCursor(sessionId, messageId, timeCompleted);
|
void saveSessionCursor(sessionId, messageId, timeCompleted);
|
||||||
|
|
||||||
if (ENABLE_EMPTY_RESPONSE_DETECTION) {
|
|
||||||
const completedMessage = getMessageFromStore(sessionId, messageId);
|
|
||||||
if (completedMessage) {
|
|
||||||
const storedParts = Array.isArray(completedMessage.parts) ? completedMessage.parts : [];
|
|
||||||
const eventParts = partsArray;
|
|
||||||
|
|
||||||
const combinedParts: Part[] = [...storedParts];
|
|
||||||
for (let i = 0; i < eventParts.length; i++) {
|
|
||||||
const rawPart = eventParts[i];
|
|
||||||
if (!rawPart) continue;
|
|
||||||
|
|
||||||
const normalized: Part = {
|
|
||||||
...rawPart,
|
|
||||||
type: (rawPart as { type?: string }).type || 'text',
|
|
||||||
} as Part;
|
|
||||||
|
|
||||||
const alreadyPresent = combinedParts.some(
|
|
||||||
(existing) =>
|
|
||||||
existing.id === normalized.id &&
|
|
||||||
existing.type === normalized.type &&
|
|
||||||
(existing as { callID?: string }).callID === (normalized as { callID?: string }).callID
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!alreadyPresent) {
|
|
||||||
combinedParts.push(normalized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasStepMarkers = false;
|
|
||||||
let hasTextContent = false;
|
|
||||||
let hasTools = false;
|
|
||||||
let hasReasoning = false;
|
|
||||||
let hasFiles = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < combinedParts.length; i++) {
|
|
||||||
const part = combinedParts[i];
|
|
||||||
if (!part) continue;
|
|
||||||
|
|
||||||
if (part.type === 'step-start' || part.type === 'step-finish') {
|
|
||||||
hasStepMarkers = true;
|
|
||||||
} else if (part.type === 'text') {
|
|
||||||
const text = (part as { text?: string }).text;
|
|
||||||
if (typeof text === 'string' && text.trim().length > 0) {
|
|
||||||
hasTextContent = true;
|
|
||||||
}
|
|
||||||
} else if (part.type === 'tool') {
|
|
||||||
hasTools = true;
|
|
||||||
} else if (part.type === 'reasoning') {
|
|
||||||
hasReasoning = true;
|
|
||||||
} else if (part.type === 'file') {
|
|
||||||
hasFiles = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMeaningfulContent = hasTextContent || hasTools || hasReasoning || hasFiles;
|
|
||||||
const isEmptyResponse = !hasMeaningfulContent && !hasStepMarkers;
|
|
||||||
|
|
||||||
if (isEmptyResponse && !emptyResponseToastShownRef.current.has(messageId)) {
|
|
||||||
emptyResponseToastShownRef.current.add(messageId);
|
|
||||||
import('sonner').then(({ toast }) => {
|
|
||||||
toast.info('Assistant response was empty', {
|
|
||||||
description: 'Try sending your message again or rephrase it.',
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completeStreamingMessage(sessionId, messageId);
|
completeStreamingMessage(sessionId, messageId);
|
||||||
// Removed: void refreshSessionStatus();
|
// Removed: void refreshSessionStatus();
|
||||||
|
|
||||||
@@ -1536,6 +1466,7 @@ export const useEventStream = () => {
|
|||||||
import('sonner').then(({ toast }) => {
|
import('sonner').then(({ toast }) => {
|
||||||
toast.warning('Permission required', {
|
toast.warning('Permission required', {
|
||||||
description: sessionTitle,
|
description: sessionTitle,
|
||||||
|
duration: Infinity,
|
||||||
action: {
|
action: {
|
||||||
label: 'Open',
|
label: 'Open',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -1552,8 +1483,16 @@ export const useEventStream = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'permission.replied':
|
case 'permission.replied': {
|
||||||
|
const sessionId = typeof props.sessionID === 'string' ? props.sessionID : null;
|
||||||
|
const requestId =
|
||||||
|
typeof props.requestID === 'string' ? props.requestID :
|
||||||
|
typeof props.id === 'string' ? props.id : null;
|
||||||
|
if (sessionId && requestId) {
|
||||||
|
dismissPermission(sessionId, requestId);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'question.asked': {
|
case 'question.asked': {
|
||||||
if (!('sessionID' in props) || typeof props.sessionID !== 'string') {
|
if (!('sessionID' in props) || typeof props.sessionID !== 'string') {
|
||||||
@@ -1598,6 +1537,7 @@ export const useEventStream = () => {
|
|||||||
import('sonner').then(({ toast }) => {
|
import('sonner').then(({ toast }) => {
|
||||||
toast.info('Input needed', {
|
toast.info('Input needed', {
|
||||||
description: sessionTitle,
|
description: sessionTitle,
|
||||||
|
duration: Infinity,
|
||||||
action: {
|
action: {
|
||||||
label: 'Open',
|
label: 'Open',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -1680,6 +1620,7 @@ export const useEventStream = () => {
|
|||||||
completeStreamingMessage,
|
completeStreamingMessage,
|
||||||
updateMessageInfo,
|
updateMessageInfo,
|
||||||
addPermission,
|
addPermission,
|
||||||
|
dismissPermission,
|
||||||
addQuestion,
|
addQuestion,
|
||||||
dismissQuestion,
|
dismissQuestion,
|
||||||
checkConnection,
|
checkConnection,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface PermissionState {
|
|||||||
interface PermissionActions {
|
interface PermissionActions {
|
||||||
addPermission: (permission: PermissionRequest, contextData?: { currentAgentContext?: Map<string, string>, sessionAgentSelections?: Map<string, string>, getSessionAgentEditMode?: (sessionId: string, agentName: string | undefined) => string }) => void;
|
addPermission: (permission: PermissionRequest, contextData?: { currentAgentContext?: Map<string, string>, sessionAgentSelections?: Map<string, string>, getSessionAgentEditMode?: (sessionId: string, agentName: string | undefined) => string }) => void;
|
||||||
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => Promise<void>;
|
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => Promise<void>;
|
||||||
|
dismissPermission: (sessionId: string, requestId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PermissionStore = PermissionState & PermissionActions;
|
type PermissionStore = PermissionState & PermissionActions;
|
||||||
@@ -123,6 +124,16 @@ export const usePermissionStore = create<PermissionStore>()(
|
|||||||
return { permissions: newPermissions };
|
return { permissions: newPermissions };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dismissPermission: (sessionId: string, requestId: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const sessionPermissions = state.permissions.get(sessionId) || [];
|
||||||
|
const updatedPermissions = sessionPermissions.filter((p) => p.id !== requestId);
|
||||||
|
const newPermissions = new Map(state.permissions);
|
||||||
|
newPermissions.set(sessionId, updatedPermissions);
|
||||||
|
return { permissions: newPermissions };
|
||||||
|
});
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "permission-store",
|
name: "permission-store",
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ export interface SessionStore {
|
|||||||
updateSessionCompaction: (sessionId: string, compactingTimestamp?: number | null) => void;
|
updateSessionCompaction: (sessionId: string, compactingTimestamp?: number | null) => void;
|
||||||
addPermission: (permission: PermissionRequest) => void;
|
addPermission: (permission: PermissionRequest) => void;
|
||||||
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => Promise<void>;
|
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => Promise<void>;
|
||||||
|
dismissPermission: (sessionId: string, requestId: string) => void;
|
||||||
|
|
||||||
addQuestion: (question: QuestionRequest) => void;
|
addQuestion: (question: QuestionRequest) => void;
|
||||||
dismissQuestion: (sessionId: string, requestId: string) => void;
|
dismissQuestion: (sessionId: string, requestId: string) => void;
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
return usePermissionStore.getState().addPermission(permission, contextData);
|
return usePermissionStore.getState().addPermission(permission, contextData);
|
||||||
},
|
},
|
||||||
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => usePermissionStore.getState().respondToPermission(sessionId, requestId, response),
|
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => usePermissionStore.getState().respondToPermission(sessionId, requestId, response),
|
||||||
|
dismissPermission: (sessionId: string, requestId: string) => usePermissionStore.getState().dismissPermission(sessionId, requestId),
|
||||||
|
|
||||||
addQuestion: (question: QuestionRequest) => useQuestionStore.getState().addQuestion(question),
|
addQuestion: (question: QuestionRequest) => useQuestionStore.getState().addQuestion(question),
|
||||||
dismissQuestion: (sessionId: string, requestId: string) => useQuestionStore.getState().dismissQuestion(sessionId, requestId),
|
dismissQuestion: (sessionId: string, requestId: string) => useQuestionStore.getState().dismissQuestion(sessionId, requestId),
|
||||||
|
|||||||
Reference in New Issue
Block a user