mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-01 00:40:42 -07:00
feat: custom loading phrase when interactive shell requires input (#12535)
This commit is contained in:
@@ -76,6 +76,8 @@ export const useShellCommandProcessor = (
|
||||
terminalHeight?: number,
|
||||
) => {
|
||||
const [activeShellPtyId, setActiveShellPtyId] = useState<number | null>(null);
|
||||
const [lastShellOutputTime, setLastShellOutputTime] = useState<number>(0);
|
||||
|
||||
const handleShellCommand = useCallback(
|
||||
(rawQuery: PartListUnion, abortSignal: AbortSignal): boolean => {
|
||||
if (typeof rawQuery !== 'string' || rawQuery.trim() === '') {
|
||||
@@ -202,6 +204,7 @@ export const useShellCommandProcessor = (
|
||||
|
||||
// Throttle pending UI updates, but allow forced updates.
|
||||
if (shouldUpdate) {
|
||||
setLastShellOutputTime(Date.now());
|
||||
setPendingHistoryItem((prevItem) => {
|
||||
if (prevItem?.type === 'tool_group') {
|
||||
return {
|
||||
@@ -366,5 +369,5 @@ export const useShellCommandProcessor = (
|
||||
],
|
||||
);
|
||||
|
||||
return { handleShellCommand, activeShellPtyId };
|
||||
return { handleShellCommand, activeShellPtyId, lastShellOutputTime };
|
||||
};
|
||||
|
||||
@@ -136,6 +136,7 @@ export const useGeminiStream = (
|
||||
markToolsAsSubmitted,
|
||||
setToolCallsForDisplay,
|
||||
cancelAllToolCalls,
|
||||
lastToolOutputTime,
|
||||
] = useReactToolScheduler(
|
||||
async (completedToolCallsFromScheduler) => {
|
||||
// This onComplete is called when ALL scheduled tools for a given batch are done.
|
||||
@@ -211,17 +212,18 @@ export const useGeminiStream = (
|
||||
await done;
|
||||
setIsResponding(false);
|
||||
}, []);
|
||||
const { handleShellCommand, activeShellPtyId } = useShellCommandProcessor(
|
||||
addItem,
|
||||
setPendingHistoryItem,
|
||||
onExec,
|
||||
onDebugMessage,
|
||||
config,
|
||||
geminiClient,
|
||||
setShellInputFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
);
|
||||
const { handleShellCommand, activeShellPtyId, lastShellOutputTime } =
|
||||
useShellCommandProcessor(
|
||||
addItem,
|
||||
setPendingHistoryItem,
|
||||
onExec,
|
||||
onDebugMessage,
|
||||
config,
|
||||
geminiClient,
|
||||
setShellInputFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
);
|
||||
|
||||
const activePtyId = activeShellPtyId || activeToolPtyId;
|
||||
|
||||
@@ -681,8 +683,9 @@ export const useGeminiStream = (
|
||||
[FinishReason.UNEXPECTED_TOOL_CALL]:
|
||||
'Response stopped due to unexpected tool call.',
|
||||
[FinishReason.IMAGE_PROHIBITED_CONTENT]:
|
||||
'Response stopped due to prohibited content.',
|
||||
[FinishReason.NO_IMAGE]: 'Response stopped due to no image.',
|
||||
'Response stopped due to prohibited image content.',
|
||||
[FinishReason.NO_IMAGE]:
|
||||
'Response stopped because no image was generated.',
|
||||
};
|
||||
|
||||
const message = finishReasonMessages[finishReason];
|
||||
@@ -1348,6 +1351,8 @@ export const useGeminiStream = (
|
||||
storage,
|
||||
]);
|
||||
|
||||
const lastOutputTime = Math.max(lastToolOutputTime, lastShellOutputTime);
|
||||
|
||||
return {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
@@ -1359,5 +1364,6 @@ export const useGeminiStream = (
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
};
|
||||
};
|
||||
|
||||
39
packages/cli/src/ui/hooks/useInactivityTimer.ts
Normal file
39
packages/cli/src/ui/hooks/useInactivityTimer.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Returns true after a specified delay of inactivity.
|
||||
* Inactivity is defined as 'trigger' not changing for 'delayMs' milliseconds.
|
||||
*
|
||||
* @param isActive Whether the timer should be running.
|
||||
* @param trigger Any value that, when changed, resets the inactivity timer.
|
||||
* @param delayMs The delay in milliseconds before considering the state inactive.
|
||||
*/
|
||||
export const useInactivityTimer = (
|
||||
isActive: boolean,
|
||||
trigger: unknown,
|
||||
delayMs: number = 5000,
|
||||
): boolean => {
|
||||
const [isInactive, setIsInactive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setIsInactive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsInactive(false);
|
||||
const timer = setTimeout(() => {
|
||||
setIsInactive(true);
|
||||
}, delayMs);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isActive, trigger, delayMs]);
|
||||
|
||||
return isInactive;
|
||||
};
|
||||
@@ -9,8 +9,12 @@ import { act } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useLoadingIndicator } from './useLoadingIndicator.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { PHRASE_CHANGE_INTERVAL_MS } from './usePhraseCycler.js';
|
||||
import {
|
||||
PHRASE_CHANGE_INTERVAL_MS,
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
} from './usePhraseCycler.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
|
||||
describe('useLoadingIndicator', () => {
|
||||
beforeEach(() => {
|
||||
@@ -25,18 +29,33 @@ describe('useLoadingIndicator', () => {
|
||||
|
||||
const renderLoadingIndicatorHook = (
|
||||
initialStreamingState: StreamingState,
|
||||
initialIsInteractiveShellWaiting: boolean = false,
|
||||
initialLastOutputTime: number = 0,
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
||||
function TestComponent({
|
||||
streamingState,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
}: {
|
||||
streamingState: StreamingState;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
}) {
|
||||
hookResult = useLoadingIndicator(streamingState);
|
||||
hookResult = useLoadingIndicator(
|
||||
streamingState,
|
||||
undefined,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(
|
||||
<TestComponent streamingState={initialStreamingState} />,
|
||||
<TestComponent
|
||||
streamingState={initialStreamingState}
|
||||
isInteractiveShellWaiting={initialIsInteractiveShellWaiting}
|
||||
lastOutputTime={initialLastOutputTime}
|
||||
/>,
|
||||
);
|
||||
return {
|
||||
result: {
|
||||
@@ -44,8 +63,11 @@ describe('useLoadingIndicator', () => {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: { streamingState: StreamingState }) =>
|
||||
rerender(<TestComponent {...newProps} />),
|
||||
rerender: (newProps: {
|
||||
streamingState: StreamingState;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
}) => rerender(<TestComponent {...newProps} />),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,6 +80,28 @@ describe('useLoadingIndicator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should show interactive shell waiting phrase when isInteractiveShellWaiting is true after 5s', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
// Initially should be witty phrase or tip
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
result.current.currentLoadingPhrase,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
|
||||
expect(result.current.currentLoadingPhrase).toBe(
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reflect values when Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty for subsequent phrases
|
||||
const { result } = renderLoadingIndicatorHook(StreamingState.Responding);
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useState, useEffect, useRef } from 'react'; // Added useRef
|
||||
export const useLoadingIndicator = (
|
||||
streamingState: StreamingState,
|
||||
customWittyPhrases?: string[],
|
||||
isInteractiveShellWaiting: boolean = false,
|
||||
lastOutputTime: number = 0,
|
||||
) => {
|
||||
const [timerResetKey, setTimerResetKey] = useState(0);
|
||||
const isTimerActive = streamingState === StreamingState.Responding;
|
||||
@@ -23,6 +25,8 @@ export const useLoadingIndicator = (
|
||||
const currentLoadingPhrase = usePhraseCycler(
|
||||
isPhraseCyclingActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
customWittyPhrases,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Text } from 'ink';
|
||||
import {
|
||||
usePhraseCycler,
|
||||
PHRASE_CHANGE_INTERVAL_MS,
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
} from './usePhraseCycler.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
@@ -19,13 +20,23 @@ import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
const TestComponent = ({
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting = false,
|
||||
lastOutputTime = 0,
|
||||
customPhrases,
|
||||
}: {
|
||||
isActive: boolean;
|
||||
isWaiting: boolean;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
customPhrases?: string[];
|
||||
}) => {
|
||||
const phrase = usePhraseCycler(isActive, isWaiting, customPhrases);
|
||||
const phrase = usePhraseCycler(
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
customPhrases,
|
||||
);
|
||||
return <Text>{phrase}</Text>;
|
||||
};
|
||||
|
||||
@@ -57,6 +68,102 @@ describe('usePhraseCycler', () => {
|
||||
expect(lastFrame()).toBe('Waiting for user confirmation...');
|
||||
});
|
||||
|
||||
it('should show interactive shell waiting message when isInteractiveShellWaiting is true after 5s', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={1}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
// Should still be showing a witty phrase or tip initially
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
lastFrame(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
expect(lastFrame()).toBe(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('should reset interactive shell waiting timer when lastOutputTime changes', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={1000}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Advance 3 seconds
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
});
|
||||
// Should still be witty phrase or tip
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
lastFrame(),
|
||||
);
|
||||
|
||||
// Update lastOutputTime
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={4000}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Advance another 3 seconds (total 6s from start, but only 3s from last output)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
});
|
||||
// Should STILL be witty phrase or tip because timer reset
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
lastFrame(),
|
||||
);
|
||||
|
||||
// Advance another 2 seconds (total 5s from last output)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
expect(lastFrame()).toBe(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('should prioritize interactive shell waiting over normal waiting after 5s', async () => {
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent isActive={true} isWaiting={true} />,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Waiting for user confirmation...');
|
||||
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={true}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={1}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
expect(lastFrame()).toBe(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('should not cycle phrases if isActive is false and not waiting', async () => {
|
||||
const { lastFrame } = render(
|
||||
<TestComponent isActive={false} isWaiting={false} />,
|
||||
|
||||
@@ -5,20 +5,28 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { SHELL_FOCUS_HINT_DELAY_MS } from '../constants.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import { useInactivityTimer } from './useInactivityTimer.js';
|
||||
|
||||
export const PHRASE_CHANGE_INTERVAL_MS = 15000;
|
||||
export const INTERACTIVE_SHELL_WAITING_PHRASE =
|
||||
'Interactive shell awaiting input... press Ctrl+f to focus shell';
|
||||
|
||||
/**
|
||||
* Custom hook to manage cycling through loading phrases.
|
||||
* @param isActive Whether the phrase cycling should be active.
|
||||
* @param isWaiting Whether to show a specific waiting phrase.
|
||||
* @param isInteractiveShellWaiting Whether an interactive shell is waiting for input but not focused.
|
||||
* @param customPhrases Optional list of custom phrases to use.
|
||||
* @returns The current loading phrase.
|
||||
*/
|
||||
export const usePhraseCycler = (
|
||||
isActive: boolean,
|
||||
isWaiting: boolean,
|
||||
isInteractiveShellWaiting: boolean,
|
||||
lastOutputTime: number = 0,
|
||||
customPhrases?: string[],
|
||||
) => {
|
||||
const loadingPhrases =
|
||||
@@ -29,66 +37,79 @@ export const usePhraseCycler = (
|
||||
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
|
||||
loadingPhrases[0],
|
||||
);
|
||||
const showShellFocusHint = useInactivityTimer(
|
||||
isInteractiveShellWaiting && lastOutputTime > 0,
|
||||
lastOutputTime,
|
||||
SHELL_FOCUS_HINT_DELAY_MS,
|
||||
);
|
||||
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const hasShownFirstRequestTipRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Always clear on re-run
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
|
||||
if (isInteractiveShellWaiting && showShellFocusHint) {
|
||||
setCurrentLoadingPhrase(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWaiting) {
|
||||
setCurrentLoadingPhrase('Waiting for user confirmation...');
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
} else if (isActive) {
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
}
|
||||
|
||||
const setRandomPhrase = () => {
|
||||
if (customPhrases && customPhrases.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * customPhrases.length);
|
||||
setCurrentLoadingPhrase(customPhrases[randomIndex]);
|
||||
} else {
|
||||
let phraseList;
|
||||
// Show a tip on the first request after startup, then continue with 1/6 chance
|
||||
if (!hasShownFirstRequestTipRef.current) {
|
||||
// Show a tip during the first request
|
||||
phraseList = INFORMATIVE_TIPS;
|
||||
hasShownFirstRequestTipRef.current = true;
|
||||
} else {
|
||||
// Roughly 1 in 6 chance to show a tip after the first request
|
||||
const showTip = Math.random() < 1 / 6;
|
||||
phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES;
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * phraseList.length);
|
||||
setCurrentLoadingPhrase(phraseList[randomIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
// Select an initial random phrase
|
||||
setRandomPhrase();
|
||||
|
||||
phraseIntervalRef.current = setInterval(() => {
|
||||
// Select a new random phrase
|
||||
setRandomPhrase();
|
||||
}, PHRASE_CHANGE_INTERVAL_MS);
|
||||
} else {
|
||||
// Idle or other states, clear the phrase interval
|
||||
// and reset to the first phrase for next active state.
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
setCurrentLoadingPhrase(loadingPhrases[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
setCurrentLoadingPhrase(loadingPhrases[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const setRandomPhrase = () => {
|
||||
if (customPhrases && customPhrases.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * customPhrases.length);
|
||||
setCurrentLoadingPhrase(customPhrases[randomIndex]);
|
||||
} else {
|
||||
let phraseList;
|
||||
// Show a tip on the first request after startup, then continue with 1/6 chance
|
||||
if (!hasShownFirstRequestTipRef.current) {
|
||||
// Show a tip during the first request
|
||||
phraseList = INFORMATIVE_TIPS;
|
||||
hasShownFirstRequestTipRef.current = true;
|
||||
} else {
|
||||
// Roughly 1 in 6 chance to show a tip after the first request
|
||||
const showTip = Math.random() < 1 / 6;
|
||||
phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES;
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * phraseList.length);
|
||||
setCurrentLoadingPhrase(phraseList[randomIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
// Select an initial random phrase
|
||||
setRandomPhrase();
|
||||
|
||||
phraseIntervalRef.current = setInterval(() => {
|
||||
// Select a new random phrase
|
||||
setRandomPhrase();
|
||||
}, PHRASE_CHANGE_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isActive, isWaiting, customPhrases, loadingPhrases]);
|
||||
}, [
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
customPhrases,
|
||||
loadingPhrases,
|
||||
showShellFocusHint,
|
||||
]);
|
||||
|
||||
return currentLoadingPhrase;
|
||||
};
|
||||
|
||||
@@ -74,10 +74,12 @@ export function useReactToolScheduler(
|
||||
MarkToolsAsSubmittedFn,
|
||||
React.Dispatch<React.SetStateAction<TrackedToolCall[]>>,
|
||||
CancelAllFn,
|
||||
number,
|
||||
] {
|
||||
const [toolCallsForDisplay, setToolCallsForDisplay] = useState<
|
||||
TrackedToolCall[]
|
||||
>([]);
|
||||
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
|
||||
|
||||
// Store callbacks in refs to keep them up-to-date without causing re-renders.
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
@@ -93,6 +95,7 @@ export function useReactToolScheduler(
|
||||
|
||||
const outputUpdateHandler: OutputUpdateHandler = useCallback(
|
||||
(toolCallId, outputChunk) => {
|
||||
setLastToolOutputTime(Date.now());
|
||||
setToolCallsForDisplay((prevCalls) =>
|
||||
prevCalls.map((tc) => {
|
||||
if (tc.request.callId === toolCallId && tc.status === 'executing') {
|
||||
@@ -208,6 +211,7 @@ export function useReactToolScheduler(
|
||||
markToolsAsSubmitted,
|
||||
setToolCallsForDisplay,
|
||||
cancelAllToolCalls,
|
||||
lastToolOutputTime,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user