mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Fix so rewind starts at the bottom and loadHistory refreshes static content. (#17335)
This commit is contained in:
@@ -239,6 +239,7 @@ describe('RewindViewer', () => {
|
|||||||
|
|
||||||
// Select
|
// Select
|
||||||
act(() => {
|
act(() => {
|
||||||
|
stdin.write('\x1b[A'); // Move up from 'Stay at current position'
|
||||||
stdin.write('\r');
|
stdin.write('\r');
|
||||||
});
|
});
|
||||||
expect(lastFrame()).toMatchSnapshot('confirmation-dialog');
|
expect(lastFrame()).toMatchSnapshot('confirmation-dialog');
|
||||||
@@ -280,6 +281,7 @@ describe('RewindViewer', () => {
|
|||||||
|
|
||||||
// Select
|
// Select
|
||||||
act(() => {
|
act(() => {
|
||||||
|
stdin.write('\x1b[A'); // Move up from 'Stay at current position'
|
||||||
stdin.write('\r'); // Select
|
stdin.write('\r'); // Select
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -188,8 +188,10 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
|
|||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
<BaseSelectionList
|
<BaseSelectionList
|
||||||
items={items}
|
items={items}
|
||||||
|
initialIndex={items.length - 1}
|
||||||
isFocused={true}
|
isFocused={true}
|
||||||
showNumbers={false}
|
showNumbers={false}
|
||||||
|
wrapAround={false}
|
||||||
onSelect={(item: MessageRecord) => {
|
onSelect={(item: MessageRecord) => {
|
||||||
const userPrompt = item;
|
const userPrompt = item;
|
||||||
if (userPrompt && userPrompt.id) {
|
if (userPrompt && userPrompt.id) {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ exports[`RewindViewer > Content Filtering > 'removes reference markers' 1`] = `
|
|||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● some command @file │
|
│ some command @file │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -22,10 +22,10 @@ exports[`RewindViewer > Content Filtering > 'strips expanded MCP resource conten
|
|||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● read @server3:mcp://demo-resource hello │
|
│ read @server3:mcp://demo-resource hello │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -72,13 +72,13 @@ exports[`RewindViewer > Navigation > handles 'down' navigation > after-down 1`]
|
|||||||
│ Q1 │
|
│ Q1 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ ● Q2 │
|
│ Q2 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Q3 │
|
│ Q3 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -98,30 +98,7 @@ exports[`RewindViewer > Navigation > handles 'up' navigation > after-up 1`] = `
|
|||||||
│ Q2 │
|
│ Q2 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Q3 │
|
│ ● Q3 │
|
||||||
│ No files have been changed │
|
|
||||||
│ │
|
|
||||||
│ ● Stay at current position │
|
|
||||||
│ Cancel rewind and stay here │
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
|
|
||||||
│ │
|
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-down 1`] = `
|
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
||||||
│ │
|
|
||||||
│ > Rewind │
|
|
||||||
│ │
|
|
||||||
│ ● Q1 │
|
|
||||||
│ No files have been changed │
|
|
||||||
│ │
|
|
||||||
│ Q2 │
|
|
||||||
│ No files have been changed │
|
|
||||||
│ │
|
|
||||||
│ Q3 │
|
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ Stay at current position │
|
||||||
@@ -133,7 +110,7 @@ exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-down 1`]
|
|||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-up 1`] = `
|
exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-down 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
@@ -156,15 +133,38 @@ exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-up 1`] =
|
|||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-up 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ │
|
||||||
|
│ > Rewind │
|
||||||
|
│ │
|
||||||
|
│ Q1 │
|
||||||
|
│ No files have been changed │
|
||||||
|
│ │
|
||||||
|
│ Q2 │
|
||||||
|
│ No files have been changed │
|
||||||
|
│ │
|
||||||
|
│ ● Q3 │
|
||||||
|
│ No files have been changed │
|
||||||
|
│ │
|
||||||
|
│ Stay at current position │
|
||||||
|
│ Cancel rewind and stay here │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
|
||||||
|
│ │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`RewindViewer > Rendering > renders 'a single interaction' 1`] = `
|
exports[`RewindViewer > Rendering > renders 'a single interaction' 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● Hello │
|
│ Hello │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -178,11 +178,11 @@ exports[`RewindViewer > Rendering > renders 'full text for selected item' 1`] =
|
|||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● 1 │
|
│ 1 │
|
||||||
│ 2... │
|
│ 2... │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -210,13 +210,13 @@ exports[`RewindViewer > updates content when conversation changes (background up
|
|||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● Message 1 │
|
│ Message 1 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Message 2 │
|
│ Message 2 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -230,10 +230,10 @@ exports[`RewindViewer > updates content when conversation changes (background up
|
|||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● Message 1 │
|
│ Message 1 │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -251,11 +251,11 @@ exports[`RewindViewer > updates selection and expansion on navigation > after-do
|
|||||||
│ Line B... │
|
│ Line B... │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ ● Line 1 │
|
│ Line 1 │
|
||||||
│ Line 2... │
|
│ Line 2... │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
@@ -269,7 +269,7 @@ exports[`RewindViewer > updates selection and expansion on navigation > initial-
|
|||||||
│ │
|
│ │
|
||||||
│ > Rewind │
|
│ > Rewind │
|
||||||
│ │
|
│ │
|
||||||
│ ● Line A │
|
│ Line A │
|
||||||
│ Line B... │
|
│ Line B... │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
@@ -277,7 +277,7 @@ exports[`RewindViewer > updates selection and expansion on navigation > initial-
|
|||||||
│ Line 2... │
|
│ Line 2... │
|
||||||
│ No files have been changed │
|
│ No files have been changed │
|
||||||
│ │
|
│ │
|
||||||
│ Stay at current position │
|
│ ● Stay at current position │
|
||||||
│ Cancel rewind and stay here │
|
│ Cancel rewind and stay here │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ describe('BaseSelectionList', () => {
|
|||||||
onHighlight: mockOnHighlight,
|
onHighlight: mockOnHighlight,
|
||||||
isFocused,
|
isFocused,
|
||||||
showNumbers,
|
showNumbers,
|
||||||
|
wrapAround: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface BaseSelectionListProps<
|
|||||||
showNumbers?: boolean;
|
showNumbers?: boolean;
|
||||||
showScrollArrows?: boolean;
|
showScrollArrows?: boolean;
|
||||||
maxItemsToShow?: number;
|
maxItemsToShow?: number;
|
||||||
|
wrapAround?: boolean;
|
||||||
renderItem: (item: TItem, context: RenderItemContext) => React.ReactNode;
|
renderItem: (item: TItem, context: RenderItemContext) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ export function BaseSelectionList<
|
|||||||
showNumbers = true,
|
showNumbers = true,
|
||||||
showScrollArrows = false,
|
showScrollArrows = false,
|
||||||
maxItemsToShow = 10,
|
maxItemsToShow = 10,
|
||||||
|
wrapAround = true,
|
||||||
renderItem,
|
renderItem,
|
||||||
}: BaseSelectionListProps<T, TItem>): React.JSX.Element {
|
}: BaseSelectionListProps<T, TItem>): React.JSX.Element {
|
||||||
const { activeIndex } = useSelectionList({
|
const { activeIndex } = useSelectionList({
|
||||||
@@ -68,6 +70,7 @@ export function BaseSelectionList<
|
|||||||
onHighlight,
|
onHighlight,
|
||||||
isFocused,
|
isFocused,
|
||||||
showNumbers,
|
showNumbers,
|
||||||
|
wrapAround,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [scrollOffset, setScrollOffset] = useState(0);
|
const [scrollOffset, setScrollOffset] = useState(0);
|
||||||
|
|||||||
@@ -156,11 +156,22 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const setupProcessorHook = async (
|
const setupProcessorHook = async (
|
||||||
builtinCommands: SlashCommand[] = [],
|
options: {
|
||||||
fileCommands: SlashCommand[] = [],
|
builtinCommands?: SlashCommand[];
|
||||||
mcpCommands: SlashCommand[] = [],
|
fileCommands?: SlashCommand[];
|
||||||
setIsProcessing = vi.fn(),
|
mcpCommands?: SlashCommand[];
|
||||||
|
setIsProcessing?: (isProcessing: boolean) => void;
|
||||||
|
refreshStatic?: () => void;
|
||||||
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
|
const {
|
||||||
|
builtinCommands = [],
|
||||||
|
fileCommands = [],
|
||||||
|
mcpCommands = [],
|
||||||
|
setIsProcessing = vi.fn(),
|
||||||
|
refreshStatic = vi.fn(),
|
||||||
|
} = options;
|
||||||
|
|
||||||
mockBuiltinLoadCommands.mockResolvedValue(Object.freeze(builtinCommands));
|
mockBuiltinLoadCommands.mockResolvedValue(Object.freeze(builtinCommands));
|
||||||
mockFileLoadCommands.mockResolvedValue(Object.freeze(fileCommands));
|
mockFileLoadCommands.mockResolvedValue(Object.freeze(fileCommands));
|
||||||
mockMcpLoadCommands.mockResolvedValue(Object.freeze(mcpCommands));
|
mockMcpLoadCommands.mockResolvedValue(Object.freeze(mcpCommands));
|
||||||
@@ -177,7 +188,7 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
mockAddItem,
|
mockAddItem,
|
||||||
mockClearItems,
|
mockClearItems,
|
||||||
mockLoadHistory,
|
mockLoadHistory,
|
||||||
vi.fn(), // refreshStatic
|
refreshStatic,
|
||||||
vi.fn(), // toggleVimEnabled
|
vi.fn(), // toggleVimEnabled
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
{
|
{
|
||||||
@@ -234,7 +245,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
context.ui.clear();
|
context.ui.clear();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await setupProcessorHook([clearCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [clearCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.handleSlashCommand('/clear');
|
await result.current.handleSlashCommand('/clear');
|
||||||
@@ -251,7 +264,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
context.ui.clear();
|
context.ui.clear();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await setupProcessorHook([clearCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [clearCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.handleSlashCommand('/clear');
|
await result.current.handleSlashCommand('/clear');
|
||||||
@@ -271,7 +286,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
|
|
||||||
it('should call loadCommands and populate state after mounting', async () => {
|
it('should call loadCommands and populate state after mounting', async () => {
|
||||||
const testCommand = createTestCommand({ name: 'test' });
|
const testCommand = createTestCommand({ name: 'test' });
|
||||||
const result = await setupProcessorHook([testCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [testCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.slashCommands).toHaveLength(1);
|
expect(result.current.slashCommands).toHaveLength(1);
|
||||||
@@ -285,7 +302,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
|
|
||||||
it('should provide an immutable array of commands to consumers', async () => {
|
it('should provide an immutable array of commands to consumers', async () => {
|
||||||
const testCommand = createTestCommand({ name: 'test' });
|
const testCommand = createTestCommand({ name: 'test' });
|
||||||
const result = await setupProcessorHook([testCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [testCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.slashCommands).toHaveLength(1);
|
expect(result.current.slashCommands).toHaveLength(1);
|
||||||
@@ -313,7 +332,10 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
CommandKind.FILE,
|
CommandKind.FILE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await setupProcessorHook([builtinCommand], [fileCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [builtinCommand],
|
||||||
|
fileCommands: [fileCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// The service should only return one command with the name 'override'
|
// The service should only return one command with the name 'override'
|
||||||
@@ -363,7 +385,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const result = await setupProcessorHook([parentCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [parentCommand],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -397,7 +421,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const result = await setupProcessorHook([parentCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [parentCommand],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -421,7 +447,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
|
|
||||||
it('sets isProcessing to false if the the input is not a command', async () => {
|
it('sets isProcessing to false if the the input is not a command', async () => {
|
||||||
const setMockIsProcessing = vi.fn();
|
const setMockIsProcessing = vi.fn();
|
||||||
const result = await setupProcessorHook([], [], [], setMockIsProcessing);
|
const result = await setupProcessorHook({
|
||||||
|
setIsProcessing: setMockIsProcessing,
|
||||||
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.handleSlashCommand('imnotacommand');
|
await result.current.handleSlashCommand('imnotacommand');
|
||||||
@@ -437,12 +465,10 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
action: vi.fn().mockRejectedValue(new Error('oh no!')),
|
action: vi.fn().mockRejectedValue(new Error('oh no!')),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await setupProcessorHook(
|
const result = await setupProcessorHook({
|
||||||
[failCommand],
|
builtinCommands: [failCommand],
|
||||||
[],
|
setIsProcessing: setMockIsProcessing,
|
||||||
[],
|
});
|
||||||
setMockIsProcessing,
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||||
|
|
||||||
@@ -461,12 +487,10 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
action: () => new Promise((resolve) => setTimeout(resolve, 50)),
|
action: () => new Promise((resolve) => setTimeout(resolve, 50)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await setupProcessorHook(
|
const result = await setupProcessorHook({
|
||||||
[command],
|
builtinCommands: [command],
|
||||||
[],
|
setIsProcessing: mockSetIsProcessing,
|
||||||
[],
|
});
|
||||||
mockSetIsProcessing,
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
const executionPromise = act(async () => {
|
const executionPromise = act(async () => {
|
||||||
@@ -508,7 +532,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ type: 'dialog', dialog: dialogType }),
|
.mockResolvedValue({ type: 'dialog', dialog: dialogType }),
|
||||||
});
|
});
|
||||||
const result = await setupProcessorHook([command]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
});
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(result.current.slashCommands).toHaveLength(1),
|
expect(result.current.slashCommands).toHaveLength(1),
|
||||||
);
|
);
|
||||||
@@ -537,20 +563,42 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
clientHistory: [{ role: 'user', parts: [{ text: 'old prompt' }] }],
|
clientHistory: [{ role: 'user', parts: [{ text: 'old prompt' }] }],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const result = await setupProcessorHook([command]);
|
|
||||||
|
const mockRefreshStatic = vi.fn();
|
||||||
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
refreshStatic: mockRefreshStatic,
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.handleSlashCommand('/load');
|
await result.current.handleSlashCommand('/load');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ui.clear() is called which calls refreshStatic()
|
||||||
expect(mockClearItems).toHaveBeenCalledTimes(1);
|
expect(mockClearItems).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
expect(mockAddItem).toHaveBeenCalledWith(
|
||||||
{ type: 'user', text: 'old prompt' },
|
{ type: 'user', text: 'old prompt' },
|
||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call refreshStatic exactly once when ui.loadHistory is called', async () => {
|
||||||
|
const mockRefreshStatic = vi.fn();
|
||||||
|
const result = await setupProcessorHook({
|
||||||
|
refreshStatic: mockRefreshStatic,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.commandContext.ui.loadHistory([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockLoadHistory).toHaveBeenCalled();
|
||||||
|
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle a "quit" action', async () => {
|
it('should handle a "quit" action', async () => {
|
||||||
const quitAction = vi
|
const quitAction = vi
|
||||||
.fn()
|
.fn()
|
||||||
@@ -559,7 +607,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
name: 'exit',
|
name: 'exit',
|
||||||
action: quitAction,
|
action: quitAction,
|
||||||
});
|
});
|
||||||
const result = await setupProcessorHook([command]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
@@ -582,7 +632,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
CommandKind.FILE,
|
CommandKind.FILE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await setupProcessorHook([], [fileCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
fileCommands: [fileCommand],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
let actionResult;
|
let actionResult;
|
||||||
@@ -614,7 +666,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
CommandKind.MCP_PROMPT,
|
CommandKind.MCP_PROMPT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await setupProcessorHook([], [], [mcpCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
mcpCommands: [mcpCommand],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
let actionResult;
|
let actionResult;
|
||||||
@@ -637,7 +691,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
describe('Command Parsing and Matching', () => {
|
describe('Command Parsing and Matching', () => {
|
||||||
it('should be case-sensitive', async () => {
|
it('should be case-sensitive', async () => {
|
||||||
const command = createTestCommand({ name: 'test' });
|
const command = createTestCommand({ name: 'test' });
|
||||||
const result = await setupProcessorHook([command]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -663,7 +719,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
description: 'a command with an alias',
|
description: 'a command with an alias',
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
const result = await setupProcessorHook([command]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -679,7 +737,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
it('should handle extra whitespace around the command', async () => {
|
it('should handle extra whitespace around the command', async () => {
|
||||||
const action = vi.fn();
|
const action = vi.fn();
|
||||||
const command = createTestCommand({ name: 'test', action });
|
const command = createTestCommand({ name: 'test', action });
|
||||||
const result = await setupProcessorHook([command]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -692,7 +752,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
it('should handle `?` as a command prefix', async () => {
|
it('should handle `?` as a command prefix', async () => {
|
||||||
const action = vi.fn();
|
const action = vi.fn();
|
||||||
const command = createTestCommand({ name: 'help', action });
|
const command = createTestCommand({ name: 'help', action });
|
||||||
const result = await setupProcessorHook([command]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [command],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -721,7 +783,10 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
CommandKind.FILE,
|
CommandKind.FILE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await setupProcessorHook([], [fileCommand], [mcpCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
fileCommands: [fileCommand],
|
||||||
|
mcpCommands: [mcpCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// The service should only return one command with the name 'override'
|
// The service should only return one command with the name 'override'
|
||||||
@@ -757,7 +822,10 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
|
|
||||||
// The order of commands in the final loaded array is not guaranteed,
|
// The order of commands in the final loaded array is not guaranteed,
|
||||||
// so the test must work regardless of which comes first.
|
// so the test must work regardless of which comes first.
|
||||||
const result = await setupProcessorHook([quitCommand], [exitCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [quitCommand],
|
||||||
|
fileCommands: [exitCommand],
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.slashCommands).toHaveLength(2);
|
expect(result.current.slashCommands).toHaveLength(2);
|
||||||
@@ -784,7 +852,10 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
CommandKind.FILE,
|
CommandKind.FILE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await setupProcessorHook([quitCommand], [exitCommand]);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: [quitCommand],
|
||||||
|
fileCommands: [exitCommand],
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
|
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -880,7 +951,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
desc: 'command path when alias is used',
|
desc: 'command path when alias is used',
|
||||||
},
|
},
|
||||||
])('should log $desc', async ({ command, expectedLog }) => {
|
])('should log $desc', async ({ command, expectedLog }) => {
|
||||||
const result = await setupProcessorHook(loggingTestCommands);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: loggingTestCommands,
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -899,7 +972,9 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
{ command: '/bogusbogusbogus', desc: 'bogus command' },
|
{ command: '/bogusbogusbogus', desc: 'bogus command' },
|
||||||
{ command: '/unknown', desc: 'unknown command' },
|
{ command: '/unknown', desc: 'unknown command' },
|
||||||
])('should not log for $desc', async ({ command }) => {
|
])('should not log for $desc', async ({ command }) => {
|
||||||
const result = await setupProcessorHook(loggingTestCommands);
|
const result = await setupProcessorHook({
|
||||||
|
builtinCommands: loggingTestCommands,
|
||||||
|
});
|
||||||
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
await waitFor(() => expect(result.current.slashCommands).toBeDefined());
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ export const useSlashCommandProcessor = (
|
|||||||
},
|
},
|
||||||
loadHistory: (history, postLoadInput) => {
|
loadHistory: (history, postLoadInput) => {
|
||||||
loadHistory(history);
|
loadHistory(history);
|
||||||
|
refreshStatic();
|
||||||
if (postLoadInput !== undefined) {
|
if (postLoadInput !== undefined) {
|
||||||
actions.setText(postLoadInput);
|
actions.setText(postLoadInput);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ describe('useSelectionList', () => {
|
|||||||
initialIndex?: number;
|
initialIndex?: number;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
showNumbers?: boolean;
|
showNumbers?: boolean;
|
||||||
|
wrapAround?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
let hookResult: ReturnType<typeof useSelectionList>;
|
let hookResult: ReturnType<typeof useSelectionList>;
|
||||||
function TestComponent(props: typeof initialProps) {
|
function TestComponent(props: typeof initialProps) {
|
||||||
@@ -285,6 +286,39 @@ describe('useSelectionList', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Wrapping (wrapAround)', () => {
|
||||||
|
it('should wrap by default (wrapAround=true)', async () => {
|
||||||
|
const { result } = await renderSelectionListHook({
|
||||||
|
items,
|
||||||
|
initialIndex: items.length - 1,
|
||||||
|
onSelect: mockOnSelect,
|
||||||
|
});
|
||||||
|
expect(result.current.activeIndex).toBe(3);
|
||||||
|
pressKey('down');
|
||||||
|
expect(result.current.activeIndex).toBe(0);
|
||||||
|
|
||||||
|
pressKey('up');
|
||||||
|
expect(result.current.activeIndex).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not wrap when wrapAround is false', async () => {
|
||||||
|
const { result } = await renderSelectionListHook({
|
||||||
|
items,
|
||||||
|
initialIndex: items.length - 1,
|
||||||
|
onSelect: mockOnSelect,
|
||||||
|
wrapAround: false,
|
||||||
|
});
|
||||||
|
expect(result.current.activeIndex).toBe(3);
|
||||||
|
pressKey('down');
|
||||||
|
expect(result.current.activeIndex).toBe(3); // Should stay at bottom
|
||||||
|
|
||||||
|
act(() => result.current.setActiveIndex(0));
|
||||||
|
expect(result.current.activeIndex).toBe(0);
|
||||||
|
pressKey('up');
|
||||||
|
expect(result.current.activeIndex).toBe(0); // Should stay at top
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Selection (Enter)', () => {
|
describe('Selection (Enter)', () => {
|
||||||
it('should call onSelect when "return" is pressed on enabled item', async () => {
|
it('should call onSelect when "return" is pressed on enabled item', async () => {
|
||||||
await renderSelectionListHook({
|
await renderSelectionListHook({
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface UseSelectionListOptions<T> {
|
|||||||
onHighlight?: (value: T) => void;
|
onHighlight?: (value: T) => void;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
showNumbers?: boolean;
|
showNumbers?: boolean;
|
||||||
|
wrapAround?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseSelectionListResult {
|
export interface UseSelectionListResult {
|
||||||
@@ -40,6 +41,7 @@ interface SelectionListState {
|
|||||||
pendingHighlight: boolean;
|
pendingHighlight: boolean;
|
||||||
pendingSelect: boolean;
|
pendingSelect: boolean;
|
||||||
items: BaseSelectionItem[];
|
items: BaseSelectionItem[];
|
||||||
|
wrapAround: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectionListAction =
|
type SelectionListAction =
|
||||||
@@ -60,7 +62,11 @@ type SelectionListAction =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'INITIALIZE';
|
type: 'INITIALIZE';
|
||||||
payload: { initialIndex: number; items: BaseSelectionItem[] };
|
payload: {
|
||||||
|
initialIndex: number;
|
||||||
|
items: BaseSelectionItem[];
|
||||||
|
wrapAround: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'CLEAR_PENDING_FLAGS';
|
type: 'CLEAR_PENDING_FLAGS';
|
||||||
@@ -75,6 +81,7 @@ const findNextValidIndex = (
|
|||||||
currentIndex: number,
|
currentIndex: number,
|
||||||
direction: 'up' | 'down',
|
direction: 'up' | 'down',
|
||||||
items: BaseSelectionItem[],
|
items: BaseSelectionItem[],
|
||||||
|
wrapAround = true,
|
||||||
): number => {
|
): number => {
|
||||||
const len = items.length;
|
const len = items.length;
|
||||||
if (len === 0) return currentIndex;
|
if (len === 0) return currentIndex;
|
||||||
@@ -83,13 +90,34 @@ const findNextValidIndex = (
|
|||||||
const step = direction === 'down' ? 1 : -1;
|
const step = direction === 'down' ? 1 : -1;
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
|
const candidateIndex = nextIndex + step;
|
||||||
|
|
||||||
|
if (wrapAround) {
|
||||||
// Calculate the next index, wrapping around if necessary.
|
// Calculate the next index, wrapping around if necessary.
|
||||||
// We add `len` before the modulo to ensure a positive result in JS for negative steps.
|
// We add `len` before the modulo to ensure a positive result in JS for negative steps.
|
||||||
nextIndex = (nextIndex + step + len) % len;
|
nextIndex = (candidateIndex + len) % len;
|
||||||
|
} else {
|
||||||
|
if (candidateIndex < 0 || candidateIndex >= len) {
|
||||||
|
// Out of bounds and wrapping is disabled
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
nextIndex = candidateIndex;
|
||||||
|
}
|
||||||
|
|
||||||
if (!items[nextIndex]?.disabled) {
|
if (!items[nextIndex]?.disabled) {
|
||||||
return nextIndex;
|
return nextIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!wrapAround) {
|
||||||
|
// If the item is disabled and we're not wrapping, we continue searching
|
||||||
|
// in the same direction, but we must stop if we hit the bounds.
|
||||||
|
if (
|
||||||
|
(direction === 'down' && nextIndex === len - 1) ||
|
||||||
|
(direction === 'up' && nextIndex === 0)
|
||||||
|
) {
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If all items are disabled, return the original index
|
// If all items are disabled, return the original index
|
||||||
@@ -120,7 +148,7 @@ const computeInitialIndex = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (items[targetIndex]?.disabled) {
|
if (items[targetIndex]?.disabled) {
|
||||||
const nextValid = findNextValidIndex(targetIndex, 'down', items);
|
const nextValid = findNextValidIndex(targetIndex, 'down', items, true);
|
||||||
targetIndex = nextValid;
|
targetIndex = nextValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +176,13 @@ function selectionListReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'MOVE_UP': {
|
case 'MOVE_UP': {
|
||||||
const { items } = state;
|
const { items, wrapAround } = state;
|
||||||
const newIndex = findNextValidIndex(state.activeIndex, 'up', items);
|
const newIndex = findNextValidIndex(
|
||||||
|
state.activeIndex,
|
||||||
|
'up',
|
||||||
|
items,
|
||||||
|
wrapAround,
|
||||||
|
);
|
||||||
if (newIndex !== state.activeIndex) {
|
if (newIndex !== state.activeIndex) {
|
||||||
return { ...state, activeIndex: newIndex, pendingHighlight: true };
|
return { ...state, activeIndex: newIndex, pendingHighlight: true };
|
||||||
}
|
}
|
||||||
@@ -157,8 +190,13 @@ function selectionListReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'MOVE_DOWN': {
|
case 'MOVE_DOWN': {
|
||||||
const { items } = state;
|
const { items, wrapAround } = state;
|
||||||
const newIndex = findNextValidIndex(state.activeIndex, 'down', items);
|
const newIndex = findNextValidIndex(
|
||||||
|
state.activeIndex,
|
||||||
|
'down',
|
||||||
|
items,
|
||||||
|
wrapAround,
|
||||||
|
);
|
||||||
if (newIndex !== state.activeIndex) {
|
if (newIndex !== state.activeIndex) {
|
||||||
return { ...state, activeIndex: newIndex, pendingHighlight: true };
|
return { ...state, activeIndex: newIndex, pendingHighlight: true };
|
||||||
}
|
}
|
||||||
@@ -170,7 +208,7 @@ function selectionListReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'INITIALIZE': {
|
case 'INITIALIZE': {
|
||||||
const { initialIndex, items } = action.payload;
|
const { initialIndex, items, wrapAround } = action.payload;
|
||||||
const activeKey =
|
const activeKey =
|
||||||
initialIndex === state.initialIndex &&
|
initialIndex === state.initialIndex &&
|
||||||
state.activeIndex !== state.initialIndex
|
state.activeIndex !== state.initialIndex
|
||||||
@@ -186,6 +224,7 @@ function selectionListReducer(
|
|||||||
initialIndex,
|
initialIndex,
|
||||||
activeIndex: targetIndex,
|
activeIndex: targetIndex,
|
||||||
pendingHighlight: false,
|
pendingHighlight: false,
|
||||||
|
wrapAround,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +284,7 @@ export function useSelectionList<T>({
|
|||||||
onHighlight,
|
onHighlight,
|
||||||
isFocused = true,
|
isFocused = true,
|
||||||
showNumbers = false,
|
showNumbers = false,
|
||||||
|
wrapAround = true,
|
||||||
}: UseSelectionListOptions<T>): UseSelectionListResult {
|
}: UseSelectionListOptions<T>): UseSelectionListResult {
|
||||||
const baseItems = toBaseItems(items);
|
const baseItems = toBaseItems(items);
|
||||||
|
|
||||||
@@ -254,12 +294,14 @@ export function useSelectionList<T>({
|
|||||||
pendingHighlight: false,
|
pendingHighlight: false,
|
||||||
pendingSelect: false,
|
pendingSelect: false,
|
||||||
items: baseItems,
|
items: baseItems,
|
||||||
|
wrapAround,
|
||||||
});
|
});
|
||||||
const numberInputRef = useRef('');
|
const numberInputRef = useRef('');
|
||||||
const numberInputTimer = useRef<NodeJS.Timeout | null>(null);
|
const numberInputTimer = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const prevBaseItemsRef = useRef(baseItems);
|
const prevBaseItemsRef = useRef(baseItems);
|
||||||
const prevInitialIndexRef = useRef(initialIndex);
|
const prevInitialIndexRef = useRef(initialIndex);
|
||||||
|
const prevWrapAroundRef = useRef(wrapAround);
|
||||||
|
|
||||||
// Initialize/synchronize state when initialIndex or items change
|
// Initialize/synchronize state when initialIndex or items change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -268,14 +310,16 @@ export function useSelectionList<T>({
|
|||||||
baseItems,
|
baseItems,
|
||||||
);
|
);
|
||||||
const initialIndexChanged = prevInitialIndexRef.current !== initialIndex;
|
const initialIndexChanged = prevInitialIndexRef.current !== initialIndex;
|
||||||
|
const wrapAroundChanged = prevWrapAroundRef.current !== wrapAround;
|
||||||
|
|
||||||
if (baseItemsChanged || initialIndexChanged) {
|
if (baseItemsChanged || initialIndexChanged || wrapAroundChanged) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'INITIALIZE',
|
type: 'INITIALIZE',
|
||||||
payload: { initialIndex, items: baseItems },
|
payload: { initialIndex, items: baseItems, wrapAround },
|
||||||
});
|
});
|
||||||
prevBaseItemsRef.current = baseItems;
|
prevBaseItemsRef.current = baseItems;
|
||||||
prevInitialIndexRef.current = initialIndex;
|
prevInitialIndexRef.current = initialIndex;
|
||||||
|
prevWrapAroundRef.current = wrapAround;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ describe('useSessionResume', () => {
|
|||||||
1,
|
1,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
expect(mockRefreshStatic).toHaveBeenCalled();
|
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
|
||||||
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith(
|
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith(
|
||||||
clientHistory,
|
clientHistory,
|
||||||
resumedData,
|
resumedData,
|
||||||
@@ -174,7 +174,7 @@ describe('useSessionResume', () => {
|
|||||||
|
|
||||||
expect(mockHistoryManager.clearItems).toHaveBeenCalled();
|
expect(mockHistoryManager.clearItems).toHaveBeenCalled();
|
||||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||||
expect(mockRefreshStatic).toHaveBeenCalled();
|
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
|
||||||
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith([], resumedData);
|
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith([], resumedData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -338,6 +338,7 @@ describe('useSessionResume', () => {
|
|||||||
1,
|
1,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
|
||||||
expect(mockGeminiClient.resumeChat).toHaveBeenCalled();
|
expect(mockGeminiClient.resumeChat).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user