feat: tab completions for file paths (#279)
Made a PR as was requested in the #113
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ result
|
|||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
.history/
|
.history/
|
||||||
|
.zed/
|
||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ export interface MultilineTextEditorHandle {
|
|||||||
isCursorAtLastRow(): boolean;
|
isCursorAtLastRow(): boolean;
|
||||||
/** Full text contents */
|
/** Full text contents */
|
||||||
getText(): string;
|
getText(): string;
|
||||||
|
/** Move the cursor to the end of the text */
|
||||||
|
moveCursorToEnd(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultilineTextEditorInner = (
|
const MultilineTextEditorInner = (
|
||||||
@@ -372,6 +374,16 @@ const MultilineTextEditorInner = (
|
|||||||
return row === lineCount - 1;
|
return row === lineCount - 1;
|
||||||
},
|
},
|
||||||
getText: () => buffer.current.getText(),
|
getText: () => buffer.current.getText(),
|
||||||
|
moveCursorToEnd: () => {
|
||||||
|
buffer.current.move("home");
|
||||||
|
const lines = buffer.current.getText().split("\n");
|
||||||
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
|
buffer.current.move("down");
|
||||||
|
}
|
||||||
|
buffer.current.move("end");
|
||||||
|
// Force a re-render
|
||||||
|
setVersion((v) => v + 1);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
64
codex-cli/src/components/chat/terminal-chat-completions.tsx
Normal file
64
codex-cli/src/components/chat/terminal-chat-completions.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Box, Text } from "ink";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
type TextCompletionProps = {
|
||||||
|
/**
|
||||||
|
* Array of text completion options to display in the list
|
||||||
|
*/
|
||||||
|
completions: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of completion items to show at once in the view
|
||||||
|
*/
|
||||||
|
displayLimit: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index of the currently selected completion in the completions array
|
||||||
|
*/
|
||||||
|
selectedCompletion: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TerminalChatCompletions({
|
||||||
|
completions,
|
||||||
|
selectedCompletion,
|
||||||
|
displayLimit,
|
||||||
|
}: TextCompletionProps): JSX.Element {
|
||||||
|
const visibleItems = useMemo(() => {
|
||||||
|
// Try to keep selection centered in view
|
||||||
|
let startIndex = Math.max(
|
||||||
|
0,
|
||||||
|
selectedCompletion - Math.floor(displayLimit / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fix window position when at the end of the list
|
||||||
|
if (completions.length - startIndex < displayLimit) {
|
||||||
|
startIndex = Math.max(0, completions.length - displayLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endIndex = Math.min(completions.length, startIndex + displayLimit);
|
||||||
|
|
||||||
|
return completions.slice(startIndex, endIndex).map((completion, index) => ({
|
||||||
|
completion,
|
||||||
|
originalIndex: index + startIndex,
|
||||||
|
}));
|
||||||
|
}, [completions, selectedCompletion, displayLimit]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{visibleItems.map(({ completion, originalIndex }) => (
|
||||||
|
<Text
|
||||||
|
key={completion}
|
||||||
|
dimColor={originalIndex !== selectedCompletion}
|
||||||
|
underline={originalIndex === selectedCompletion}
|
||||||
|
backgroundColor={
|
||||||
|
originalIndex === selectedCompletion ? "blackBright" : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{completion}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TerminalChatCompletions;
|
||||||
@@ -8,8 +8,10 @@ import type {
|
|||||||
|
|
||||||
import MultilineTextEditor from "./multiline-editor";
|
import MultilineTextEditor from "./multiline-editor";
|
||||||
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
|
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
|
||||||
|
import TextCompletions from "./terminal-chat-completions.js";
|
||||||
import { log } from "../../utils/agent/log.js";
|
import { log } from "../../utils/agent/log.js";
|
||||||
import { loadConfig } from "../../utils/config.js";
|
import { loadConfig } from "../../utils/config.js";
|
||||||
|
import { getFileSystemSuggestions } from "../../utils/file-system-suggestions.js";
|
||||||
import { createInputItem } from "../../utils/input-utils.js";
|
import { createInputItem } from "../../utils/input-utils.js";
|
||||||
import { setSessionId } from "../../utils/session.js";
|
import { setSessionId } from "../../utils/session.js";
|
||||||
import { SLASH_COMMANDS, type SlashCommand } from "../../utils/slash-commands";
|
import { SLASH_COMMANDS, type SlashCommand } from "../../utils/slash-commands";
|
||||||
@@ -90,6 +92,8 @@ export default function TerminalChatInput({
|
|||||||
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
|
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
|
||||||
const [draftInput, setDraftInput] = useState<string>("");
|
const [draftInput, setDraftInput] = useState<string>("");
|
||||||
const [skipNextSubmit, setSkipNextSubmit] = useState<boolean>(false);
|
const [skipNextSubmit, setSkipNextSubmit] = useState<boolean>(false);
|
||||||
|
const [fsSuggestions, setFsSuggestions] = useState<Array<string>>([]);
|
||||||
|
const [selectedCompletion, setSelectedCompletion] = useState<number>(-1);
|
||||||
// Multiline text editor key to force remount after submission
|
// Multiline text editor key to force remount after submission
|
||||||
const [editorKey, setEditorKey] = useState(0);
|
const [editorKey, setEditorKey] = useState(0);
|
||||||
// Imperative handle from the multiline editor so we can query caret position
|
// Imperative handle from the multiline editor so we can query caret position
|
||||||
@@ -196,6 +200,44 @@ export default function TerminalChatInput({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!confirmationPrompt && !loading) {
|
if (!confirmationPrompt && !loading) {
|
||||||
|
if (fsSuggestions.length > 0) {
|
||||||
|
if (_key.upArrow) {
|
||||||
|
setSelectedCompletion((prev) =>
|
||||||
|
prev <= 0 ? fsSuggestions.length - 1 : prev - 1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_key.downArrow) {
|
||||||
|
setSelectedCompletion((prev) =>
|
||||||
|
prev >= fsSuggestions.length - 1 ? 0 : prev + 1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_key.tab && selectedCompletion >= 0) {
|
||||||
|
const words = input.trim().split(/\s+/);
|
||||||
|
const selected = fsSuggestions[selectedCompletion];
|
||||||
|
|
||||||
|
if (words.length > 0 && selected) {
|
||||||
|
words[words.length - 1] = selected;
|
||||||
|
const newText = words.join(" ");
|
||||||
|
setInput(newText);
|
||||||
|
// Force remount of the editor with the new text
|
||||||
|
setEditorKey((k) => k + 1);
|
||||||
|
|
||||||
|
// We need to move the cursor to the end after editor remounts
|
||||||
|
setTimeout(() => {
|
||||||
|
editorRef.current?.moveCursorToEnd?.();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
setFsSuggestions([]);
|
||||||
|
setSelectedCompletion(-1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_key.upArrow) {
|
if (_key.upArrow) {
|
||||||
// Only recall history when the caret was *already* on the very first
|
// Only recall history when the caret was *already* on the very first
|
||||||
// row *before* this key-press.
|
// row *before* this key-press.
|
||||||
@@ -241,6 +283,19 @@ export default function TerminalChatInput({
|
|||||||
}
|
}
|
||||||
// Otherwise let it propagate
|
// Otherwise let it propagate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_key.tab) {
|
||||||
|
const words = input.split(/\s+/);
|
||||||
|
const mostRecentWord = words[words.length - 1];
|
||||||
|
if (mostRecentWord === undefined || mostRecentWord === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const completions = getFileSystemSuggestions(mostRecentWord);
|
||||||
|
setFsSuggestions(completions);
|
||||||
|
if (completions.length > 0) {
|
||||||
|
setSelectedCompletion(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cached cursor position *after* we've potentially handled
|
// Update the cached cursor position *after* we've potentially handled
|
||||||
@@ -533,6 +588,8 @@ export default function TerminalChatInput({
|
|||||||
setDraftInput("");
|
setDraftInput("");
|
||||||
setSelectedSuggestion(0);
|
setSelectedSuggestion(0);
|
||||||
setInput("");
|
setInput("");
|
||||||
|
setFsSuggestions([]);
|
||||||
|
setSelectedCompletion(-1);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
setInput,
|
setInput,
|
||||||
@@ -578,7 +635,7 @@ export default function TerminalChatInput({
|
|||||||
thinkingSeconds={thinkingSeconds}
|
thinkingSeconds={thinkingSeconds}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box>
|
<Box paddingX={1}>
|
||||||
<MultilineTextEditor
|
<MultilineTextEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
onChange={(txt: string) => {
|
onChange={(txt: string) => {
|
||||||
@@ -587,6 +644,20 @@ export default function TerminalChatInput({
|
|||||||
setHistoryIndex(null);
|
setHistoryIndex(null);
|
||||||
}
|
}
|
||||||
setInput(txt);
|
setInput(txt);
|
||||||
|
|
||||||
|
// Clear tab completions if a space is typed
|
||||||
|
if (txt.endsWith(" ")) {
|
||||||
|
setFsSuggestions([]);
|
||||||
|
setSelectedCompletion(-1);
|
||||||
|
} else if (fsSuggestions.length > 0) {
|
||||||
|
// Update file suggestions as user types
|
||||||
|
const words = txt.trim().split(/\s+/);
|
||||||
|
const mostRecentWord =
|
||||||
|
words.length > 0 ? words[words.length - 1] : "";
|
||||||
|
if (mostRecentWord !== undefined) {
|
||||||
|
setFsSuggestions(getFileSystemSuggestions(mostRecentWord));
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
key={editorKey}
|
key={editorKey}
|
||||||
initialText={input}
|
initialText={input}
|
||||||
@@ -623,47 +694,51 @@ export default function TerminalChatInput({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box paddingX={2} marginBottom={1}>
|
<Box paddingX={2} marginBottom={1}>
|
||||||
<Text dimColor>
|
{isNew && !input ? (
|
||||||
{isNew && !input ? (
|
<Text dimColor>
|
||||||
<>
|
try:{" "}
|
||||||
try:{" "}
|
{suggestions.map((m, key) => (
|
||||||
{suggestions.map((m, key) => (
|
<Fragment key={key}>
|
||||||
<Fragment key={key}>
|
{key !== 0 ? " | " : ""}
|
||||||
{key !== 0 ? " | " : ""}
|
<Text
|
||||||
<Text
|
backgroundColor={
|
||||||
backgroundColor={
|
key + 1 === selectedSuggestion ? "blackBright" : ""
|
||||||
key + 1 === selectedSuggestion ? "blackBright" : ""
|
}
|
||||||
}
|
>
|
||||||
>
|
{m}
|
||||||
{m}
|
</Text>
|
||||||
</Text>
|
</Fragment>
|
||||||
</Fragment>
|
))}
|
||||||
))}
|
</Text>
|
||||||
</>
|
) : fsSuggestions.length > 0 ? (
|
||||||
) : (
|
<TextCompletions
|
||||||
<>
|
completions={fsSuggestions}
|
||||||
send q or ctrl+c to exit | send "/clear" to reset | send "/help"
|
selectedCompletion={selectedCompletion}
|
||||||
for commands | press enter to send | shift+enter for new line
|
displayLimit={5}
|
||||||
{contextLeftPercent > 25 && (
|
/>
|
||||||
<>
|
) : (
|
||||||
{" — "}
|
<Text dimColor>
|
||||||
<Text color={contextLeftPercent > 40 ? "green" : "yellow"}>
|
send q or ctrl+c to exit | send "/clear" to reset | send "/help" for
|
||||||
{Math.round(contextLeftPercent)}% context left
|
commands | press enter to send | shift+enter for new line
|
||||||
</Text>
|
{contextLeftPercent > 25 && (
|
||||||
</>
|
<>
|
||||||
)}
|
{" — "}
|
||||||
{contextLeftPercent <= 25 && (
|
<Text color={contextLeftPercent > 40 ? "green" : "yellow"}>
|
||||||
<>
|
{Math.round(contextLeftPercent)}% context left
|
||||||
{" — "}
|
</Text>
|
||||||
<Text color="red">
|
</>
|
||||||
{Math.round(contextLeftPercent)}% context left — send
|
)}
|
||||||
"/compact" to condense context
|
{contextLeftPercent <= 25 && (
|
||||||
</Text>
|
<>
|
||||||
</>
|
{" — "}
|
||||||
)}
|
<Text color="red">
|
||||||
</>
|
{Math.round(contextLeftPercent)}% context left — send
|
||||||
)}
|
"/compact" to condense context
|
||||||
</Text>
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export type TextInputProps = {
|
|||||||
* Function to call when `Enter` is pressed, where first argument is a value of the input.
|
* Function to call when `Enter` is pressed, where first argument is a value of the input.
|
||||||
*/
|
*/
|
||||||
readonly onSubmit?: (value: string) => void;
|
readonly onSubmit?: (value: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly set the cursor position to the end of the text
|
||||||
|
*/
|
||||||
|
readonly cursorToEnd?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function findPrevWordJump(prompt: string, cursorOffset: number) {
|
function findPrevWordJump(prompt: string, cursorOffset: number) {
|
||||||
@@ -90,12 +95,22 @@ function TextInput({
|
|||||||
showCursor = true,
|
showCursor = true,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
cursorToEnd = false,
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
cursorOffset: (originalValue || "").length,
|
cursorOffset: (originalValue || "").length,
|
||||||
cursorWidth: 0,
|
cursorWidth: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cursorToEnd) {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
cursorOffset: (originalValue || "").length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [cursorToEnd, originalValue, focus]);
|
||||||
|
|
||||||
const { cursorOffset, cursorWidth } = state;
|
const { cursorOffset, cursorWidth } = state;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
42
codex-cli/src/utils/file-system-suggestions.ts
Normal file
42
codex-cli/src/utils/file-system-suggestions.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export function getFileSystemSuggestions(pathPrefix: string): Array<string> {
|
||||||
|
if (!pathPrefix) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sep = path.sep;
|
||||||
|
const hasTilde = pathPrefix === "~" || pathPrefix.startsWith("~" + sep);
|
||||||
|
const expanded = hasTilde
|
||||||
|
? path.join(os.homedir(), pathPrefix.slice(1))
|
||||||
|
: pathPrefix;
|
||||||
|
|
||||||
|
const normalized = path.normalize(expanded);
|
||||||
|
const isDir = pathPrefix.endsWith(path.sep);
|
||||||
|
const base = path.basename(normalized);
|
||||||
|
|
||||||
|
const dir =
|
||||||
|
normalized === "." && !pathPrefix.startsWith("." + sep) && !hasTilde
|
||||||
|
? process.cwd()
|
||||||
|
: path.dirname(normalized);
|
||||||
|
|
||||||
|
const readDir = isDir ? path.join(dir, base) : dir;
|
||||||
|
|
||||||
|
return fs
|
||||||
|
.readdirSync(readDir)
|
||||||
|
.filter((item) => isDir || item.startsWith(base))
|
||||||
|
.map((item) => {
|
||||||
|
const fullPath = path.join(readDir, item);
|
||||||
|
const isDirectory = fs.statSync(fullPath).isDirectory();
|
||||||
|
if (isDirectory) {
|
||||||
|
return path.join(fullPath, sep);
|
||||||
|
}
|
||||||
|
return fullPath;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
73
codex-cli/tests/file-system-suggestions.test.ts
Normal file
73
codex-cli/tests/file-system-suggestions.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { getFileSystemSuggestions } from "../src/utils/file-system-suggestions";
|
||||||
|
|
||||||
|
vi.mock("fs");
|
||||||
|
vi.mock("os");
|
||||||
|
|
||||||
|
describe("getFileSystemSuggestions", () => {
|
||||||
|
const mockFs = fs as unknown as {
|
||||||
|
readdirSync: ReturnType<typeof vi.fn>;
|
||||||
|
statSync: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockOs = os as unknown as {
|
||||||
|
homedir: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array for empty prefix", () => {
|
||||||
|
expect(getFileSystemSuggestions("")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expands ~ to home directory", () => {
|
||||||
|
mockOs.homedir = vi.fn(() => "/home/testuser");
|
||||||
|
mockFs.readdirSync = vi.fn(() => ["file1.txt", "docs"]);
|
||||||
|
mockFs.statSync = vi.fn((p) => ({
|
||||||
|
isDirectory: () => path.basename(p) === "docs",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = getFileSystemSuggestions("~/");
|
||||||
|
|
||||||
|
expect(mockFs.readdirSync).toHaveBeenCalledWith("/home/testuser");
|
||||||
|
expect(result).toEqual([
|
||||||
|
path.join("/home/testuser", "file1.txt"),
|
||||||
|
path.join("/home/testuser", "docs" + path.sep),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters by prefix if not a directory", () => {
|
||||||
|
mockFs.readdirSync = vi.fn(() => ["abc.txt", "abd.txt", "xyz.txt"]);
|
||||||
|
mockFs.statSync = vi.fn((p) => ({
|
||||||
|
isDirectory: () => p.includes("abd"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = getFileSystemSuggestions("a");
|
||||||
|
expect(result).toEqual(["abc.txt", "abd.txt/"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles errors gracefully", () => {
|
||||||
|
mockFs.readdirSync = vi.fn(() => {
|
||||||
|
throw new Error("failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFileSystemSuggestions("some/path");
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes relative path", () => {
|
||||||
|
mockFs.readdirSync = vi.fn(() => ["foo", "bar"]);
|
||||||
|
mockFs.statSync = vi.fn((_p) => ({
|
||||||
|
isDirectory: () => true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = getFileSystemSuggestions("./");
|
||||||
|
expect(result).toContain("foo/");
|
||||||
|
expect(result).toContain("bar/");
|
||||||
|
});
|
||||||
|
});
|
||||||
46
codex-cli/tests/terminal-chat-completions.test.tsx
Normal file
46
codex-cli/tests/terminal-chat-completions.test.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import { renderTui } from "./ui-test-helpers.js";
|
||||||
|
import TerminalChatCompletions from "../src/components/chat/terminal-chat-completions.js";
|
||||||
|
|
||||||
|
describe("TerminalChatCompletions", () => {
|
||||||
|
const baseProps: ComponentProps<typeof TerminalChatCompletions> = {
|
||||||
|
completions: ["Option 1", "Option 2", "Option 3", "Option 4", "Option 5"],
|
||||||
|
displayLimit: 3,
|
||||||
|
selectedCompletion: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders visible completions within displayLimit", async () => {
|
||||||
|
const { lastFrameStripped } = renderTui(
|
||||||
|
<TerminalChatCompletions {...baseProps} />,
|
||||||
|
);
|
||||||
|
const frame = lastFrameStripped();
|
||||||
|
expect(frame).toContain("Option 1");
|
||||||
|
expect(frame).toContain("Option 2");
|
||||||
|
expect(frame).toContain("Option 3");
|
||||||
|
expect(frame).not.toContain("Option 4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("centers the selected completion in the visible list", async () => {
|
||||||
|
const { lastFrameStripped } = renderTui(
|
||||||
|
<TerminalChatCompletions {...baseProps} selectedCompletion={2} />,
|
||||||
|
);
|
||||||
|
const frame = lastFrameStripped();
|
||||||
|
expect(frame).toContain("Option 2");
|
||||||
|
expect(frame).toContain("Option 3");
|
||||||
|
expect(frame).toContain("Option 4");
|
||||||
|
expect(frame).not.toContain("Option 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adjusts when selectedCompletion is near the end", async () => {
|
||||||
|
const { lastFrameStripped } = renderTui(
|
||||||
|
<TerminalChatCompletions {...baseProps} selectedCompletion={4} />,
|
||||||
|
);
|
||||||
|
const frame = lastFrameStripped();
|
||||||
|
expect(frame).toContain("Option 3");
|
||||||
|
expect(frame).toContain("Option 4");
|
||||||
|
expect(frame).toContain("Option 5");
|
||||||
|
expect(frame).not.toContain("Option 2");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user