(fix) do not transitively rely on deprecated lodash deps (#175)
Signed-off-by: Thibault Sottiaux <tibo@openai.com>
This commit is contained in:
committed by
GitHub
parent
057f113c6d
commit
f3f9e41a15
30
codex-cli/package-lock.json
generated
30
codex-cli/package-lock.json
generated
@@ -13,9 +13,9 @@
|
||||
"chalk": "^5.2.0",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.1.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-type": "^20.1.0",
|
||||
"ink": "^5.2.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"marked": "^15.0.7",
|
||||
"marked-terminal": "^7.3.0",
|
||||
"meow": "^13.2.0",
|
||||
@@ -23,6 +23,7 @@
|
||||
"openai": "^4.89.0",
|
||||
"react": "^18.2.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"to-rotated": "^1.0.0",
|
||||
"use-interval": "1.4.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -3204,9 +3205,7 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
@@ -3912,23 +3911,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ink-select-input": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.0.0.tgz",
|
||||
"integrity": "sha512-2mCbn1b9xeguA3qJiaf8Sx8W4MM005wACcLKwHWWJmJ8BapjsahmQPuY2U2qyGc817IdWFjNk/K41Vn39UlO4Q==",
|
||||
"dependencies": {
|
||||
"figures": "^6.1.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"to-rotated": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ink": ">=5.0.0",
|
||||
"react": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ink-testing-library": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-3.0.0.tgz",
|
||||
@@ -4552,12 +4534,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
"chalk": "^5.2.0",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.1.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-type": "^20.1.0",
|
||||
"ink": "^5.2.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"marked": "^15.0.7",
|
||||
"marked-terminal": "^7.3.0",
|
||||
"meow": "^13.2.0",
|
||||
@@ -44,6 +44,7 @@
|
||||
"openai": "^4.89.0",
|
||||
"react": "^18.2.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"to-rotated": "^1.0.0",
|
||||
"use-interval": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
21
codex-cli/src/components/select-input/Indicator.tsx
Normal file
21
codex-cli/src/components/select-input/Indicator.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import figures from "figures";
|
||||
import { Box, Text } from "ink";
|
||||
import React from "react";
|
||||
|
||||
export type Props = {
|
||||
readonly isSelected?: boolean;
|
||||
};
|
||||
|
||||
function Indicator({ isSelected = false }: Props): JSX.Element {
|
||||
return (
|
||||
<Box marginRight={1}>
|
||||
{isSelected ? (
|
||||
<Text color="blue">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Indicator;
|
||||
13
codex-cli/src/components/select-input/Item.tsx
Normal file
13
codex-cli/src/components/select-input/Item.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Text } from "ink";
|
||||
import * as React from "react";
|
||||
|
||||
export type Props = {
|
||||
readonly isSelected?: boolean;
|
||||
readonly label: string;
|
||||
};
|
||||
|
||||
function Item({ isSelected = false, label }: Props): JSX.Element {
|
||||
return <Text color={isSelected ? "blue" : undefined}>{label}</Text>;
|
||||
}
|
||||
|
||||
export default Item;
|
||||
189
codex-cli/src/components/select-input/select-input.tsx
Normal file
189
codex-cli/src/components/select-input/select-input.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import Indicator, { type Props as IndicatorProps } from "./Indicator.js";
|
||||
import ItemComponent, { type Props as ItemProps } from "./Item.js";
|
||||
import isEqual from "fast-deep-equal";
|
||||
import { Box, useInput } from "ink";
|
||||
import React, {
|
||||
type FC,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import arrayToRotated from "to-rotated";
|
||||
|
||||
type Props<V> = {
|
||||
/**
|
||||
* Items to display in a list. Each item must be an object and have `label` and `value` props, it may also optionally have a `key` prop.
|
||||
* If no `key` prop is provided, `value` will be used as the item key.
|
||||
*/
|
||||
readonly items?: Array<Item<V>>;
|
||||
|
||||
/**
|
||||
* Listen to user's input. Useful in case there are multiple input components at the same time and input must be "routed" to a specific component.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
readonly isFocused?: boolean;
|
||||
|
||||
/**
|
||||
* Index of initially-selected item in `items` array.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
readonly initialIndex?: number;
|
||||
|
||||
/**
|
||||
* Number of items to display.
|
||||
*/
|
||||
readonly limit?: number;
|
||||
|
||||
/**
|
||||
* Custom component to override the default indicator component.
|
||||
*/
|
||||
readonly indicatorComponent?: FC<IndicatorProps>;
|
||||
|
||||
/**
|
||||
* Custom component to override the default item component.
|
||||
*/
|
||||
readonly itemComponent?: FC<ItemProps>;
|
||||
|
||||
/**
|
||||
* Function to call when user selects an item. Item object is passed to that function as an argument.
|
||||
*/
|
||||
readonly onSelect?: (item: Item<V>) => void;
|
||||
|
||||
/**
|
||||
* Function to call when user highlights an item. Item object is passed to that function as an argument.
|
||||
*/
|
||||
readonly onHighlight?: (item: Item<V>) => void;
|
||||
};
|
||||
|
||||
export type Item<V> = {
|
||||
key?: string;
|
||||
label: string;
|
||||
value: V;
|
||||
};
|
||||
|
||||
function SelectInput<V>({
|
||||
items = [],
|
||||
isFocused = true,
|
||||
initialIndex = 0,
|
||||
indicatorComponent = Indicator,
|
||||
itemComponent = ItemComponent,
|
||||
limit: customLimit,
|
||||
onSelect,
|
||||
onHighlight,
|
||||
}: Props<V>): JSX.Element {
|
||||
const hasLimit =
|
||||
typeof customLimit === "number" && items.length > customLimit;
|
||||
const limit = hasLimit ? Math.min(customLimit, items.length) : items.length;
|
||||
const lastIndex = limit - 1;
|
||||
const [rotateIndex, setRotateIndex] = useState(
|
||||
initialIndex > lastIndex ? lastIndex - initialIndex : 0,
|
||||
);
|
||||
const [selectedIndex, setSelectedIndex] = useState(
|
||||
initialIndex ? (initialIndex > lastIndex ? lastIndex : initialIndex) : 0,
|
||||
);
|
||||
const previousItems = useRef<Array<Item<V>>>(items);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isEqual(
|
||||
previousItems.current.map((item) => item.value),
|
||||
items.map((item) => item.value),
|
||||
)
|
||||
) {
|
||||
setRotateIndex(0);
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
|
||||
previousItems.current = items;
|
||||
}, [items]);
|
||||
|
||||
useInput(
|
||||
useCallback(
|
||||
(input, key) => {
|
||||
if (input === "k" || key.upArrow) {
|
||||
const lastIndex = (hasLimit ? limit : items.length) - 1;
|
||||
const atFirstIndex = selectedIndex === 0;
|
||||
const nextIndex = hasLimit ? selectedIndex : lastIndex;
|
||||
const nextRotateIndex = atFirstIndex ? rotateIndex + 1 : rotateIndex;
|
||||
const nextSelectedIndex = atFirstIndex
|
||||
? nextIndex
|
||||
: selectedIndex - 1;
|
||||
|
||||
setRotateIndex(nextRotateIndex);
|
||||
setSelectedIndex(nextSelectedIndex);
|
||||
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, nextRotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
if (typeof onHighlight === "function") {
|
||||
onHighlight(slicedItems[nextSelectedIndex]!);
|
||||
}
|
||||
}
|
||||
|
||||
if (input === "j" || key.downArrow) {
|
||||
const atLastIndex =
|
||||
selectedIndex === (hasLimit ? limit : items.length) - 1;
|
||||
const nextIndex = hasLimit ? selectedIndex : 0;
|
||||
const nextRotateIndex = atLastIndex ? rotateIndex - 1 : rotateIndex;
|
||||
const nextSelectedIndex = atLastIndex ? nextIndex : selectedIndex + 1;
|
||||
|
||||
setRotateIndex(nextRotateIndex);
|
||||
setSelectedIndex(nextSelectedIndex);
|
||||
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, nextRotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
if (typeof onHighlight === "function") {
|
||||
onHighlight(slicedItems[nextSelectedIndex]!);
|
||||
}
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, rotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
if (typeof onSelect === "function") {
|
||||
onSelect(slicedItems[selectedIndex]!);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
hasLimit,
|
||||
limit,
|
||||
rotateIndex,
|
||||
selectedIndex,
|
||||
items,
|
||||
onSelect,
|
||||
onHighlight,
|
||||
],
|
||||
),
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, rotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{slicedItems.map((item, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<Box key={item.key ?? String(item.value)}>
|
||||
{React.createElement(indicatorComponent, { isSelected })}
|
||||
{React.createElement(itemComponent, { ...item, isSelected })}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectInput;
|
||||
@@ -1,6 +1,6 @@
|
||||
import SelectInput from "./select-input/select-input.js";
|
||||
import TextInput from "./vendor/ink-text-input.js";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import SelectInput from "ink-select-input";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export type TypeaheadItem = { label: string; value: string };
|
||||
|
||||
@@ -9,14 +9,13 @@ import * as React from "react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock <ink-select-input> so we can capture the props that TypeaheadOverlay
|
||||
// Mock <select-input> so we can capture the props that TypeaheadOverlay
|
||||
// forwards without rendering the real component (which would require a full
|
||||
// Ink TTY environment).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let receivedItems: Array<{ label: string; value: string }> | null = null;
|
||||
|
||||
vi.mock("ink-select-input", () => {
|
||||
vi.mock("../src/components/select-input/select-input.js", () => {
|
||||
return {
|
||||
default: (props: any) => {
|
||||
receivedItems = props.items;
|
||||
|
||||
Reference in New Issue
Block a user