feat(h5): show rotating loading tips during agent-run execution
Surface backend loading_tip SSE events in chat UI with shared tip pool and styles for long-running TKMind 智趣 tasks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -121,6 +121,12 @@ function createAgentRunFailedError(message: string) {
|
||||
return error;
|
||||
}
|
||||
|
||||
function extractLoadingTipFromRunEvent(data: unknown): string | null {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
const text = (data as { text?: unknown }).text;
|
||||
return typeof text === 'string' && text.trim() ? text.trim() : null;
|
||||
}
|
||||
|
||||
function errorCode(error: unknown) {
|
||||
if (error instanceof ApiError) return error.code;
|
||||
if (error && typeof error === 'object' && 'code' in error) {
|
||||
@@ -396,6 +402,7 @@ export function useTKMindChat(
|
||||
const [rechargeForced, setRechargeForced] = useState(false);
|
||||
const [subscribePrompt, setSubscribePrompt] = useState(false);
|
||||
const [activeNotification, setActiveNotification] = useState<UserNotification | null>(null);
|
||||
const [taskLoadingTip, setTaskLoadingTip] = useState<string | null>(null);
|
||||
const seenNotificationIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const activeRequestId = useRef<string | null>(null);
|
||||
@@ -424,11 +431,43 @@ export function useTKMindChat(
|
||||
const pendingSubmitQueueRef = useRef<PendingChatSubmitEntry[]>([]);
|
||||
const flushingPendingSubmitRef = useRef(false);
|
||||
const pendingToolRef = useRef<ToolConfirmation | null>(null);
|
||||
const taskLoadingTipUnsubRef = useRef<(() => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
chatStateRef.current = chatState;
|
||||
}, [chatState]);
|
||||
|
||||
const clearTaskLoadingTipSubscription = useCallback(() => {
|
||||
taskLoadingTipUnsubRef.current?.();
|
||||
taskLoadingTipUnsubRef.current = null;
|
||||
setTaskLoadingTip(null);
|
||||
}, []);
|
||||
|
||||
const startTaskLoadingTipSubscription = useCallback((runId: string, isCancelled: () => boolean) => {
|
||||
clearTaskLoadingTipSubscription();
|
||||
taskLoadingTipUnsubRef.current = subscribeAgentRunEvents(
|
||||
runId,
|
||||
() => {},
|
||||
() => {},
|
||||
{
|
||||
onRunEvent: (event) => {
|
||||
if (isCancelled()) return;
|
||||
if (event.eventType !== 'loading_tip') return;
|
||||
const tip = extractLoadingTipFromRunEvent(event.data);
|
||||
if (tip) setTaskLoadingTip(tip);
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [clearTaskLoadingTipSubscription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatState === 'idle' || chatState === 'error' || chatState === 'loading') {
|
||||
clearTaskLoadingTipSubscription();
|
||||
}
|
||||
}, [chatState, clearTaskLoadingTipSubscription]);
|
||||
|
||||
useEffect(() => () => clearTaskLoadingTipSubscription(), [clearTaskLoadingTipSubscription]);
|
||||
|
||||
useEffect(() => {
|
||||
pendingToolRef.current = pendingTool;
|
||||
}, [pendingTool]);
|
||||
@@ -1560,6 +1599,7 @@ export function useTKMindChat(
|
||||
...(options?.goalRunId ? { goalRunId: options.goalRunId } : {}),
|
||||
},
|
||||
);
|
||||
startTaskLoadingTipSubscription(createdRun.id, () => submitToken !== connectTokenRef.current);
|
||||
const runSessionId = createdRun.sessionId ?? activeSessionId;
|
||||
if (runSessionId && !isDirectChatSessionId(runSessionId)) {
|
||||
activeSessionId = runSessionId;
|
||||
@@ -1729,6 +1769,7 @@ export function useTKMindChat(
|
||||
ensureProvider,
|
||||
loadProjectMemory,
|
||||
refreshSessions,
|
||||
startTaskLoadingTipSubscription,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1902,6 +1943,7 @@ export function useTKMindChat(
|
||||
chatStateRef.current = 'waiting';
|
||||
setError(null);
|
||||
agentRunPendingRef.current = true;
|
||||
startTaskLoadingTipSubscription(createdRun.id, () => submitToken !== connectTokenRef.current);
|
||||
|
||||
try {
|
||||
const finishedRun =
|
||||
@@ -1960,7 +2002,7 @@ export function useTKMindChat(
|
||||
activeRequestId.current = null;
|
||||
}
|
||||
},
|
||||
[clearActiveRequestMissingTimer, scheduleReplyRecoverySync, subscribeToSession],
|
||||
[clearActiveRequestMissingTimer, scheduleReplyRecoverySync, subscribeToSession, startTaskLoadingTipSubscription],
|
||||
);
|
||||
|
||||
const newSession = useCallback(async () => {
|
||||
@@ -2159,6 +2201,7 @@ export function useTKMindChat(
|
||||
error,
|
||||
notice,
|
||||
pendingTool,
|
||||
taskLoadingTip,
|
||||
memoryLoading,
|
||||
userMemoryLoading,
|
||||
canUseProjectMemory,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { pickTkmindLoadingTip } from '../utils/tkmindLoadingTips';
|
||||
|
||||
const ROTATE_MS = 4_500;
|
||||
|
||||
export function useTaskLoadingTip(active: boolean, externalTip?: string | null) {
|
||||
const [localTip, setLocalTip] = useState<string | null>(null);
|
||||
const recentTipsRef = useRef<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || externalTip) {
|
||||
setLocalTip(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const rotate = () => {
|
||||
const picked = pickTkmindLoadingTip({ exclude: recentTipsRef.current.slice(-3) });
|
||||
recentTipsRef.current = [...recentTipsRef.current, picked.text].slice(-6);
|
||||
setLocalTip(picked.text);
|
||||
};
|
||||
|
||||
rotate();
|
||||
const timer = window.setInterval(rotate, ROTATE_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [active, externalTip]);
|
||||
|
||||
return externalTip ?? localTip;
|
||||
}
|
||||
Reference in New Issue
Block a user