chore: use reducer

This commit is contained in:
Jack Wotherspoon
2026-03-14 21:45:03 +01:00
parent 90bdba9059
commit 27a56f4f7a
2 changed files with 81 additions and 35 deletions
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import type { Config, PolicyRule } from '@google/gemini-cli-core';
import { CommandKind, type SlashCommand } from './types.js';
import { MessageType } from '../types.js';
@@ -66,11 +65,13 @@ const policiesDialogAction: NonNullable<SlashCommand['action']> = async (
return {
type: 'custom_dialog' as const,
component: React.createElement(PoliciesDialog, {
rules,
toolDisplayNames,
onClose: () => context.ui.removeComponent(),
}),
component: (
<PoliciesDialog
rules={rules}
toolDisplayNames={toolDisplayNames}
onClose={() => context.ui.removeComponent()}
/>
),
};
};
@@ -5,7 +5,7 @@
*/
import type React from 'react';
import { useState, useMemo, useEffect, useCallback } from 'react';
import { useState, useMemo, useEffect, useCallback, useReducer } from 'react';
import { Box, Text } from 'ink';
import { AsyncFzf, type FzfResultItem } from 'fzf';
import { PolicyDecision, type PolicyRule } from '@google/gemini-cli-core';
@@ -25,6 +25,67 @@ import { isNarrowWidth } from '../utils/isNarrowWidth.js';
const ITEM_HEIGHT = 2;
interface NavigationState {
activeTabIndex: number;
activeIndex: number;
scrollOffset: number;
}
type NavigationAction =
| { type: 'MOVE_UP'; maxItemsToShow: number }
| { type: 'MOVE_DOWN'; maxItemsToShow: number; totalItems: number }
| { type: 'CYCLE_TAB'; direction: number; numTabs: number }
| { type: 'RESET_SCROLL' };
function navigationReducer(
state: NavigationState,
action: NavigationAction,
): NavigationState {
switch (action.type) {
case 'MOVE_UP': {
if (state.activeIndex <= 0) return state;
const nextIndex = state.activeIndex - 1;
return {
...state,
activeIndex: nextIndex,
scrollOffset:
nextIndex < state.scrollOffset ? nextIndex : state.scrollOffset,
};
}
case 'MOVE_DOWN': {
if (state.activeIndex >= action.totalItems - 1) return state;
const nextIndex = state.activeIndex + 1;
return {
...state,
activeIndex: nextIndex,
scrollOffset:
nextIndex >= state.scrollOffset + action.maxItemsToShow
? nextIndex - action.maxItemsToShow + 1
: state.scrollOffset,
};
}
case 'CYCLE_TAB': {
return {
...state,
activeTabIndex:
(state.activeTabIndex + action.direction + action.numTabs) %
action.numTabs,
activeIndex: 0,
scrollOffset: 0,
};
}
case 'RESET_SCROLL': {
return {
...state,
activeIndex: 0,
scrollOffset: 0,
};
}
default:
return state;
}
}
interface PoliciesDialogProps {
rules: readonly PolicyRule[];
toolDisplayNames: Map<string, string>;
@@ -58,9 +119,10 @@ export function PoliciesDialog({
const { terminalHeight, terminalWidth, staticExtraHeight, constrainHeight } =
useUIState();
const [activeTabIndex, setActiveTabIndex] = useState(0);
const [activeIndex, setActiveIndex] = useState(0);
const [scrollOffset, setScrollOffset] = useState(0);
const [{ activeTabIndex, activeIndex, scrollOffset }, dispatch] = useReducer(
navigationReducer,
{ activeTabIndex: 0, activeIndex: 0, scrollOffset: 0 },
);
const [searchQuery, setSearchQuery] = useState('');
const [filteredItems, setFilteredItems] = useState<PolicyListItem[] | null>(
null,
@@ -187,16 +249,9 @@ export function PoliciesDialog({
// Items to display (filtered or all)
const displayItems = filteredItems ?? allTabItems;
// Reset scroll when tab changes
useEffect(() => {
setActiveIndex(0);
setScrollOffset(0);
}, [activeTabIndex]);
// Reset scroll when filtered items change
useEffect(() => {
setActiveIndex(0);
setScrollOffset(0);
dispatch({ type: 'RESET_SCROLL' });
}, [filteredItems]);
// Search buffer
@@ -232,31 +287,21 @@ export function PoliciesDialog({
(key.name === 'tab' && key.shift)
? -1
: 1;
setActiveTabIndex(
(prev) => (prev + direction + TABS.length) % TABS.length,
);
dispatch({ type: 'CYCLE_TAB', direction, numTabs: TABS.length });
return;
}
// Up/Down navigation (no wrap-around)
if (keyMatchers[Command.DIALOG_NAVIGATION_UP](key)) {
if (activeIndex > 0) {
const newIndex = activeIndex - 1;
setActiveIndex(newIndex);
if (newIndex < scrollOffset) {
setScrollOffset(newIndex);
}
}
dispatch({ type: 'MOVE_UP', maxItemsToShow: effectiveMaxItemsToShow });
return;
}
if (keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key)) {
if (activeIndex < displayItems.length - 1) {
const newIndex = activeIndex + 1;
setActiveIndex(newIndex);
if (newIndex >= scrollOffset + effectiveMaxItemsToShow) {
setScrollOffset(newIndex - effectiveMaxItemsToShow + 1);
}
}
dispatch({
type: 'MOVE_DOWN',
maxItemsToShow: effectiveMaxItemsToShow,
totalItems: displayItems.length,
});
return;
}