mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 15:31:27 +00:00
refactor ui for stop hook and userPromptSubmit
This commit is contained in:
parent
3776825c2d
commit
a5c6084222
6 changed files with 346 additions and 6 deletions
|
|
@ -230,6 +230,16 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
{itemForDisplay.type === 'btw' && itemForDisplay.btw && (
|
||||
<BtwMessage btw={itemForDisplay.btw} />
|
||||
)}
|
||||
{itemForDisplay.type === 'user_prompt_submit_blocked' && (
|
||||
<ErrorMessage
|
||||
text={`UserPromptSubmit operation blocked by hook:\n${itemForDisplay.reason}\n\nOriginal prompt: ${itemForDisplay.originalPrompt}`}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'stop_hook_loop' && (
|
||||
<InfoMessage
|
||||
text={`Ran ${itemForDisplay.iterationCount} stop hooks\n ⎿ Stop hook reason: ${itemForDisplay.reasons[itemForDisplay.reasons.length - 1]}`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3226,4 +3226,209 @@ describe('useGeminiStream', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserPromptSubmitBlocked Event', () => {
|
||||
it('should handle UserPromptSubmitBlocked event and add blocked history item', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.UserPromptSubmitBlocked,
|
||||
value: {
|
||||
reason: 'Hook blocked due to security policy',
|
||||
originalPrompt: 'This is the original user prompt',
|
||||
},
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('This is the original user prompt');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'user_prompt_submit_blocked',
|
||||
reason: 'Hook blocked due to security policy',
|
||||
originalPrompt: 'This is the original user prompt',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
// Verify streaming state transitions correctly
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
});
|
||||
|
||||
it('should move pending history item before adding UserPromptSubmitBlocked event', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Partial response before block',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.UserPromptSubmitBlocked,
|
||||
value: {
|
||||
reason: 'Security violation detected',
|
||||
originalPrompt: 'Execute system command',
|
||||
},
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Execute system command');
|
||||
});
|
||||
|
||||
// Verify content was added first
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
text: 'Partial response before block',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
// Then verify blocked event was added
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'user_prompt_submit_blocked',
|
||||
reason: 'Security violation detected',
|
||||
originalPrompt: 'Execute system command',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('StopHookLoop Event', () => {
|
||||
it('should handle StopHookLoop event and add stop hook loop history item', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.StopHookLoop,
|
||||
value: {
|
||||
iterationCount: 3,
|
||||
reasons: [
|
||||
'Reason 1: Continue analysis',
|
||||
'Reason 2: More details needed',
|
||||
'Reason 3: Incomplete response',
|
||||
],
|
||||
},
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('test query with stop hooks');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'stop_hook_loop',
|
||||
iterationCount: 3,
|
||||
reasons: [
|
||||
'Reason 1: Continue analysis',
|
||||
'Reason 2: More details needed',
|
||||
'Reason 3: Incomplete response',
|
||||
],
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
// Verify streaming state transitions correctly
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
});
|
||||
|
||||
it('should move pending history item before adding StopHookLoop event', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Initial response before loop',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.StopHookLoop,
|
||||
value: {
|
||||
iterationCount: 5,
|
||||
reasons: ['Hook reason 1', 'Hook reason 2'],
|
||||
},
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('query triggering stop hooks');
|
||||
});
|
||||
|
||||
// Verify content was added first
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
text: 'Initial response before loop',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
// Then verify stop hook loop event was added
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'stop_hook_loop',
|
||||
iterationCount: 5,
|
||||
reasons: ['Hook reason 1', 'Hook reason 2'],
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single iteration StopHookLoop event', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.StopHookLoop,
|
||||
value: {
|
||||
iterationCount: 1,
|
||||
reasons: ['Single hook execution'],
|
||||
},
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('single iteration query');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'stop_hook_loop',
|
||||
iterationCount: 1,
|
||||
reasons: ['Single hook execution'],
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -972,6 +972,48 @@ export const useGeminiStream = (
|
|||
});
|
||||
}, [handleLoopDetectionConfirmation]);
|
||||
|
||||
const handleUserPromptSubmitBlockedEvent = useCallback(
|
||||
(
|
||||
value: { reason: string; originalPrompt: string },
|
||||
userMessageTimestamp: number,
|
||||
) => {
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: 'user_prompt_submit_blocked',
|
||||
reason: value.reason,
|
||||
originalPrompt: value.originalPrompt,
|
||||
} as HistoryItemWithoutId,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
);
|
||||
|
||||
const handleStopHookLoopEvent = useCallback(
|
||||
(
|
||||
value: { iterationCount: number; reasons: string[] },
|
||||
userMessageTimestamp: number,
|
||||
) => {
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: 'stop_hook_loop',
|
||||
iterationCount: value.iterationCount,
|
||||
reasons: value.reasons,
|
||||
} as HistoryItemWithoutId,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
);
|
||||
|
||||
const processGeminiStreamEvents = useCallback(
|
||||
async (
|
||||
stream: AsyncIterable<GeminiEvent>,
|
||||
|
|
@ -1061,6 +1103,15 @@ export const useGeminiStream = (
|
|||
userMessageTimestamp,
|
||||
);
|
||||
break;
|
||||
case ServerGeminiEventType.UserPromptSubmitBlocked:
|
||||
handleUserPromptSubmitBlockedEvent(
|
||||
event.value,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
break;
|
||||
case ServerGeminiEventType.StopHookLoop:
|
||||
handleStopHookLoopEvent(event.value, userMessageTimestamp);
|
||||
break;
|
||||
default: {
|
||||
// enforces exhaustive switch-case
|
||||
const unreachable: never = event;
|
||||
|
|
@ -1089,6 +1140,8 @@ export const useGeminiStream = (
|
|||
setThought,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
handleUserPromptSubmitBlockedEvent,
|
||||
handleStopHookLoopEvent,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -361,6 +361,26 @@ export type HistoryItemBtw = HistoryItemBase & {
|
|||
btw: BtwProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* UserPromptSubmit hook blocked event.
|
||||
* Displayed when a UserPromptSubmit hook blocks the user's prompt.
|
||||
*/
|
||||
export type HistoryItemUserPromptSubmitBlocked = HistoryItemBase & {
|
||||
type: 'user_prompt_submit_blocked';
|
||||
reason: string;
|
||||
originalPrompt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop hook loop event.
|
||||
* Displayed when Stop hooks create a loop, forcing the agent to continue.
|
||||
*/
|
||||
export type HistoryItemStopHookLoop = HistoryItemBase & {
|
||||
type: 'stop_hook_loop';
|
||||
iterationCount: number;
|
||||
reasons: string[];
|
||||
};
|
||||
|
||||
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
||||
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
||||
// 'tools' in historyItem.
|
||||
|
|
@ -395,7 +415,9 @@ export type HistoryItemWithoutId =
|
|||
| HistoryItemArenaAgentComplete
|
||||
| HistoryItemArenaSessionComplete
|
||||
| HistoryItemInsightProgress
|
||||
| HistoryItemBtw;
|
||||
| HistoryItemBtw
|
||||
| HistoryItemUserPromptSubmitBlocked
|
||||
| HistoryItemStopHookLoop;
|
||||
|
||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue