Files
llmx/codex-cli/src/components/chat/terminal-chat-input.tsx

749 lines
22 KiB
TypeScript
Raw Normal View History

import type { ReviewDecision } from "../../utils/agent/review.js";
import type { HistoryEntry } from "../../utils/storage/command-history.js";
import type {
ResponseInputItem,
ResponseItem,
} from "openai/resources/responses/responses.mjs";
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
import { log, isLoggingEnabled } from "../../utils/agent/log.js";
import { loadConfig } from "../../utils/config.js";
import { createInputItem } from "../../utils/input-utils.js";
import { setSessionId } from "../../utils/session.js";
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
import { SLASH_COMMANDS, type SlashCommand } from "../../utils/slash-commands";
import {
loadCommandHistory,
addToHistory,
} from "../../utils/storage/command-history.js";
import { clearTerminal, onExit } from "../../utils/terminal.js";
import TextInput from "../vendor/ink-text-input.js";
import { Box, Text, useApp, useInput, useStdin } from "ink";
import { fileURLToPath } from "node:url";
import React, { useCallback, useState, Fragment, useEffect } from "react";
import { useInterval } from "use-interval";
const suggestions = [
"explain this codebase to me",
"fix any build errors",
"are there any bugs in my code?",
];
export default function TerminalChatInput({
isNew,
loading,
submitInput,
confirmationPrompt,
feat: shell command explanation option (#173) # Shell Command Explanation Option ## Description This PR adds an option to explain shell commands when the user is prompted to approve them (Fixes #110). When reviewing a shell command, users can now select "Explain this command" to get a detailed explanation of what the command does before deciding whether to approve or reject it. ## Changes - Added a new "EXPLAIN" option to the `ReviewDecision` enum - Updated the command review UI to include an "Explain this command (x)" option - Implemented the logic to send the command to the LLM for explanation using the same model as the agent - Added a display for the explanation in the command review UI - Updated all relevant components to pass the explanation through the component tree ## Benefits - Improves user understanding of shell commands before approving them - Reduces the risk of approving potentially harmful commands - Enhances the educational aspect of the tool, helping users learn about shell commands - Maintains the same workflow with minimal UI changes ## Testing - Manually tested the explanation feature with various shell commands - Verified that the explanation is displayed correctly in the UI - Confirmed that the user can still approve or reject the command after viewing the explanation ## Screenshots ![improved_shell_explanation_demo](https://github.com/user-attachments/assets/05923481-29db-4eba-9cc6-5e92301d2be0) ## Additional Notes The explanation is generated using the same model as the agent, ensuring consistency in the quality and style of explanations. --------- Signed-off-by: crazywolf132 <crazywolf132@gmail.com>
2025-04-18 06:28:58 +10:00
explanation,
submitConfirmation,
setLastResponseId,
setItems,
contextLeftPercent,
openOverlay,
openModelOverlay,
openApprovalOverlay,
openHelpOverlay,
onCompact,
interruptAgent,
active,
thinkingSeconds,
items = [],
}: {
isNew: boolean;
loading: boolean;
submitInput: (input: Array<ResponseInputItem>) => void;
confirmationPrompt: React.ReactNode | null;
feat: shell command explanation option (#173) # Shell Command Explanation Option ## Description This PR adds an option to explain shell commands when the user is prompted to approve them (Fixes #110). When reviewing a shell command, users can now select "Explain this command" to get a detailed explanation of what the command does before deciding whether to approve or reject it. ## Changes - Added a new "EXPLAIN" option to the `ReviewDecision` enum - Updated the command review UI to include an "Explain this command (x)" option - Implemented the logic to send the command to the LLM for explanation using the same model as the agent - Added a display for the explanation in the command review UI - Updated all relevant components to pass the explanation through the component tree ## Benefits - Improves user understanding of shell commands before approving them - Reduces the risk of approving potentially harmful commands - Enhances the educational aspect of the tool, helping users learn about shell commands - Maintains the same workflow with minimal UI changes ## Testing - Manually tested the explanation feature with various shell commands - Verified that the explanation is displayed correctly in the UI - Confirmed that the user can still approve or reject the command after viewing the explanation ## Screenshots ![improved_shell_explanation_demo](https://github.com/user-attachments/assets/05923481-29db-4eba-9cc6-5e92301d2be0) ## Additional Notes The explanation is generated using the same model as the agent, ensuring consistency in the quality and style of explanations. --------- Signed-off-by: crazywolf132 <crazywolf132@gmail.com>
2025-04-18 06:28:58 +10:00
explanation?: string;
submitConfirmation: (
decision: ReviewDecision,
customDenyMessage?: string,
) => void;
setLastResponseId: (lastResponseId: string) => void;
setItems: React.Dispatch<React.SetStateAction<Array<ResponseItem>>>;
contextLeftPercent: number;
openOverlay: () => void;
openModelOverlay: () => void;
openApprovalOverlay: () => void;
openHelpOverlay: () => void;
onCompact: () => void;
interruptAgent: () => void;
active: boolean;
thinkingSeconds: number;
// New: current conversation items so we can include them in bug reports
items?: Array<ResponseItem>;
}): React.ReactElement {
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
// Slash command suggestion index
const [selectedSlashSuggestion, setSelectedSlashSuggestion] =
useState<number>(0);
const app = useApp();
const [selectedSuggestion, setSelectedSuggestion] = useState<number>(0);
const [input, setInput] = useState("");
const [history, setHistory] = useState<Array<HistoryEntry>>([]);
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
const [draftInput, setDraftInput] = useState<string>("");
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
const [skipNextSubmit, setSkipNextSubmit] = useState<boolean>(false);
// Load command history on component mount
useEffect(() => {
async function loadHistory() {
const historyEntries = await loadCommandHistory();
setHistory(historyEntries);
}
loadHistory();
}, []);
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
// Reset slash suggestion index when input prefix changes
useEffect(() => {
if (input.trim().startsWith("/")) {
setSelectedSlashSuggestion(0);
}
}, [input]);
useInput(
(_input, _key) => {
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
// Slash command navigation: up/down to select, enter to fill
if (!confirmationPrompt && !loading && input.trim().startsWith("/")) {
const prefix = input.trim();
const matches = SLASH_COMMANDS.filter((cmd: SlashCommand) =>
cmd.command.startsWith(prefix),
);
if (matches.length > 0) {
if (_key.tab) {
// Cycle and fill slash command suggestions on Tab
const len = matches.length;
// Determine new index based on shift state
const nextIdx = _key.shift
? selectedSlashSuggestion <= 0
? len - 1
: selectedSlashSuggestion - 1
: selectedSlashSuggestion >= len - 1
? 0
: selectedSlashSuggestion + 1;
setSelectedSlashSuggestion(nextIdx);
// Autocomplete the command in the input
const match = matches[nextIdx];
if (!match) {
return;
}
const cmd = match.command;
setInput(cmd);
setDraftInput(cmd);
return;
}
if (_key.upArrow) {
setSelectedSlashSuggestion((prev) =>
prev <= 0 ? matches.length - 1 : prev - 1,
);
return;
}
if (_key.downArrow) {
setSelectedSlashSuggestion((prev) =>
prev < 0 || prev >= matches.length - 1 ? 0 : prev + 1,
);
return;
}
if (_key.return) {
// Execute the currently selected slash command
const selIdx = selectedSlashSuggestion;
const cmdObj = matches[selIdx];
if (cmdObj) {
const cmd = cmdObj.command;
setInput("");
setDraftInput("");
setSelectedSlashSuggestion(0);
switch (cmd) {
case "/history":
openOverlay();
break;
case "/help":
openHelpOverlay();
break;
case "/compact":
onCompact();
break;
case "/model":
openModelOverlay();
break;
case "/approval":
openApprovalOverlay();
break;
case "/bug":
onSubmit(cmd);
break;
default:
break;
}
}
return;
}
}
}
if (!confirmationPrompt && !loading) {
if (_key.upArrow) {
if (history.length > 0) {
if (historyIndex == null) {
setDraftInput(input);
}
let newIndex: number;
if (historyIndex == null) {
newIndex = history.length - 1;
} else {
newIndex = Math.max(0, historyIndex - 1);
}
setHistoryIndex(newIndex);
setInput(history[newIndex]?.command ?? "");
}
return;
}
if (_key.downArrow) {
if (historyIndex == null) {
return;
}
const newIndex = historyIndex + 1;
if (newIndex >= history.length) {
setHistoryIndex(null);
setInput(draftInput);
} else {
setHistoryIndex(newIndex);
setInput(history[newIndex]?.command ?? "");
}
return;
}
}
if (input.trim() === "" && isNew) {
if (_key.tab) {
setSelectedSuggestion(
(s) => (s + (_key.shift ? -1 : 1)) % (suggestions.length + 1),
);
} else if (selectedSuggestion && _key.return) {
const suggestion = suggestions[selectedSuggestion - 1] || "";
setInput("");
setSelectedSuggestion(0);
submitInput([
{
role: "user",
content: [{ type: "input_text", text: suggestion }],
type: "message",
},
]);
}
} else if (_input === "\u0003" || (_input === "c" && _key.ctrl)) {
setTimeout(() => {
app.exit();
onExit();
process.exit(0);
}, 60);
}
},
{ isActive: active },
);
const onSubmit = useCallback(
async (value: string) => {
const inputValue = value.trim();
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
// If the user only entered a slash, do not send a chat message
if (inputValue === "/") {
setInput("");
return;
}
// Skip this submit if we just autocompleted a slash command
if (skipNextSubmit) {
setSkipNextSubmit(false);
return;
}
if (!inputValue) {
return;
}
if (inputValue === "/history") {
setInput("");
openOverlay();
return;
}
if (inputValue === "/help") {
setInput("");
openHelpOverlay();
return;
}
if (inputValue === "/compact") {
setInput("");
onCompact();
return;
}
if (inputValue.startsWith("/model")) {
setInput("");
openModelOverlay();
return;
}
if (inputValue.startsWith("/approval")) {
setInput("");
openApprovalOverlay();
return;
}
if (inputValue === "q" || inputValue === ":q" || inputValue === "exit") {
setInput("");
// wait one 60ms frame
setTimeout(() => {
app.exit();
onExit();
process.exit(0);
}, 60);
return;
} else if (inputValue === "/clear" || inputValue === "clear") {
setInput("");
setSessionId("");
setLastResponseId("");
clearTerminal();
// Emit a system message to confirm the clear action. We *append*
// it so Ink's <Static> treats it as new output and actually renders it.
setItems((prev) => [
...prev,
{
id: `clear-${Date.now()}`,
type: "message",
role: "system",
content: [{ type: "input_text", text: "Context cleared" }],
},
]);
return;
} else if (inputValue === "/clearhistory") {
setInput("");
// Import clearCommandHistory function to avoid circular dependencies
// Using dynamic import to lazy-load the function
import("../../utils/storage/command-history.js").then(
async ({ clearCommandHistory }) => {
await clearCommandHistory();
setHistory([]);
// Emit a system message to confirm the history clear action
setItems((prev) => [
...prev,
{
id: `clearhistory-${Date.now()}`,
type: "message",
role: "system",
content: [
{ type: "input_text", text: "Command history cleared" },
],
},
]);
},
);
return;
} else if (inputValue === "/bug") {
// Generate a GitHub bug report URL prefilled with session details
setInput("");
try {
// Dynamically import dependencies to avoid unnecessary bundle size
const [{ default: open }, os] = await Promise.all([
import("open"),
import("node:os"),
]);
// Lazy import CLI_VERSION to avoid circular deps
const { CLI_VERSION } = await import("../../utils/session.js");
const { buildBugReportUrl } = await import(
"../../utils/bug-report.js"
);
const url = buildBugReportUrl({
items: items ?? [],
cliVersion: CLI_VERSION,
model: loadConfig().model ?? "unknown",
platform: [os.platform(), os.arch(), os.release()]
.map((s) => `\`${s}\``)
.join(" | "),
});
// Open the URL in the user's default browser
await open(url, { wait: false });
// Inform the user in the chat history
setItems((prev) => [
...prev,
{
id: `bugreport-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: "📋 Opened browser to file a bug report. Please include any context that might help us fix the issue!",
},
],
},
]);
} catch (error) {
// If anything went wrong, notify the user
setItems((prev) => [
...prev,
{
id: `bugreport-error-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: `⚠️ Failed to create bug report URL: ${error}`,
},
],
},
]);
}
return;
fix: handle invalid commands (#304) ### What is added? I extend the if-else blocks with an additional condition where the commands validity is checked. This only applies for entered inputs that start with '/' and are a single word. This isn't necessarily restrictive from the previous behavior of the program. When an invalid command is detected, an error message is printed with a direction to retrieve command list. ### Why is it added? There are three main reasons for this change **1. Model Hallucination**: When invalid commands are passed as prompts to models, models hallucinate behavior. Since there was a fall through in invalid commands, the models took these as prompts and hallucinated that they completed the prompted task. An example of this behavior is below. In the case of this example, the model though they had access to `/clearhistory` tool where in reality that isn't the case. A before and after effect of this tool is below: ![img](https://github.com/user-attachments/assets/3166f151-d5d0-46d6-9ba7-c7e64ff35e4a) ![img2](https://github.com/user-attachments/assets/69934306-af68-423d-a5f0-9d922be01d27) **2. Saves Users Credits and Time**: Since false commands and invalid commands aren't processed by the model, the user doesn't spend money on stuff that could have been mitigated much easily. The time argument is especially applicable for reasoning models. **3. Saves GPU Time**: GPU time is valuable, and it is not necessary to spend it on unnecessary/invalid requests. ### Code Explanation If no command is matched, we check if the inputValue start with `/` which indicated the input is a command (I will address the case where it is isn't below). If the inputValue start with `/` we enter the else if statement. I used a nested if statement for readability and further extendability in the future. There are alternative ways to check besides regex, but regex is a short code and looks clean. **Check Conditions**: The reason why I only check single word(command) case is that to allow prompts where the user might decide to start with `/` and aren't commands. The nested if statements also come in handy where in the future other contributors might want to extend this checking. The code passes type, lint and test checks.
2025-04-18 07:50:27 +02:00
} else if (inputValue.startsWith("/")) {
// Handle invalid/unrecognized commands.
// Only single-word inputs starting with '/' (e.g., /command) that are not recognized are caught here.
// Any other input, including those starting with '/' but containing spaces
// (e.g., "/command arg"), will fall through and be treated as a regular prompt.
const trimmed = inputValue.trim();
if (/^\/\S+$/.test(trimmed)) {
setInput("");
setItems((prev) => [
...prev,
{
id: `invalidcommand-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: `Invalid command "${trimmed}". Use /help to retrieve the list of commands.`,
},
],
},
]);
return;
}
}
// detect image file paths for dynamic inclusion
const images: Array<string> = [];
let text = inputValue;
// markdown-style image syntax: ![alt](path)
text = text.replace(/!\[[^\]]*?\]\(([^)]+)\)/g, (_m, p1: string) => {
images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1);
return "";
});
// quoted file paths ending with common image extensions (e.g. '/path/to/img.png')
text = text.replace(
/['"]([^'"]+?\.(?:png|jpe?g|gif|bmp|webp|svg))['"]/gi,
(_m, p1: string) => {
images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1);
return "";
},
);
// bare file paths ending with common image extensions
text = text.replace(
// eslint-disable-next-line no-useless-escape
/\b(?:\.[\/\\]|[\/\\]|[A-Za-z]:[\/\\])?[\w-]+(?:[\/\\][\w-]+)*\.(?:png|jpe?g|gif|bmp|webp|svg)\b/gi,
(match: string) => {
images.push(
match.startsWith("file://") ? fileURLToPath(match) : match,
);
return "";
},
);
text = text.trim();
const inputItem = await createInputItem(text, images);
submitInput([inputItem]);
// Get config for history persistence
const config = loadConfig();
// Add to history and update state
const updatedHistory = await addToHistory(value, history, {
maxSize: config.history?.maxSize ?? 1000,
saveHistory: config.history?.saveHistory ?? true,
sensitivePatterns: config.history?.sensitivePatterns ?? [],
});
setHistory(updatedHistory);
setHistoryIndex(null);
setDraftInput("");
setSelectedSuggestion(0);
setInput("");
},
[
setInput,
submitInput,
setLastResponseId,
setItems,
app,
setHistory,
setHistoryIndex,
openOverlay,
openApprovalOverlay,
openModelOverlay,
openHelpOverlay,
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
history,
onCompact,
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
skipNextSubmit,
items,
],
);
if (confirmationPrompt) {
return (
<TerminalChatCommandReview
confirmationPrompt={confirmationPrompt}
onReviewCommand={submitConfirmation}
feat: allow switching approval modes when prompted to approve an edit/command (#400) Implements https://github.com/openai/codex/issues/392 When the user is in suggest or auto-edit mode and gets an approval request, they now have an option in the `Shell Command` dialog to: `Switch approval mode (v)` That option brings up the standard `Switch approval mode` dialog, allowing the user to switch into the desired mode, then drops them back to the `Shell Command` dialog's `Allow command?` prompt, allowing them to approve the current command and let the agent continue doing the rest of what it was doing without interruption. ``` ╭──────────────────────────────────────────────────────── │Shell Command │ │$ apply_patch << 'PATCH' │*** Begin Patch │*** Update File: foo.txt │@@ -1 +1 @@ │-foo │+bar │*** End Patch │PATCH │ │ │Allow command? │ │ Yes (y) │ Explain this command (x) │ Edit or give feedback (e) │ Switch approval mode (v) │ No, and keep going (n) │ No, and stop for now (esc) ╰────────────────────────────────────────────────────────╭──────────────────────────────────────────────────────── │ Switch approval mode │ Current mode: suggest │ │ │ │ ❯ suggest │ auto-edit │ full-auto │ type to search · enter to confirm · esc to cancel ╰──────────────────────────────────────────────────────── ```
2025-04-19 07:21:19 -07:00
// allow switching approval mode via 'v'
onSwitchApprovalMode={openApprovalOverlay}
feat: shell command explanation option (#173) # Shell Command Explanation Option ## Description This PR adds an option to explain shell commands when the user is prompted to approve them (Fixes #110). When reviewing a shell command, users can now select "Explain this command" to get a detailed explanation of what the command does before deciding whether to approve or reject it. ## Changes - Added a new "EXPLAIN" option to the `ReviewDecision` enum - Updated the command review UI to include an "Explain this command (x)" option - Implemented the logic to send the command to the LLM for explanation using the same model as the agent - Added a display for the explanation in the command review UI - Updated all relevant components to pass the explanation through the component tree ## Benefits - Improves user understanding of shell commands before approving them - Reduces the risk of approving potentially harmful commands - Enhances the educational aspect of the tool, helping users learn about shell commands - Maintains the same workflow with minimal UI changes ## Testing - Manually tested the explanation feature with various shell commands - Verified that the explanation is displayed correctly in the UI - Confirmed that the user can still approve or reject the command after viewing the explanation ## Screenshots ![improved_shell_explanation_demo](https://github.com/user-attachments/assets/05923481-29db-4eba-9cc6-5e92301d2be0) ## Additional Notes The explanation is generated using the same model as the agent, ensuring consistency in the quality and style of explanations. --------- Signed-off-by: crazywolf132 <crazywolf132@gmail.com>
2025-04-18 06:28:58 +10:00
explanation={explanation}
feat: allow switching approval modes when prompted to approve an edit/command (#400) Implements https://github.com/openai/codex/issues/392 When the user is in suggest or auto-edit mode and gets an approval request, they now have an option in the `Shell Command` dialog to: `Switch approval mode (v)` That option brings up the standard `Switch approval mode` dialog, allowing the user to switch into the desired mode, then drops them back to the `Shell Command` dialog's `Allow command?` prompt, allowing them to approve the current command and let the agent continue doing the rest of what it was doing without interruption. ``` ╭──────────────────────────────────────────────────────── │Shell Command │ │$ apply_patch << 'PATCH' │*** Begin Patch │*** Update File: foo.txt │@@ -1 +1 @@ │-foo │+bar │*** End Patch │PATCH │ │ │Allow command? │ │ Yes (y) │ Explain this command (x) │ Edit or give feedback (e) │ Switch approval mode (v) │ No, and keep going (n) │ No, and stop for now (esc) ╰────────────────────────────────────────────────────────╭──────────────────────────────────────────────────────── │ Switch approval mode │ Current mode: suggest │ │ │ │ ❯ suggest │ auto-edit │ full-auto │ type to search · enter to confirm · esc to cancel ╰──────────────────────────────────────────────────────── ```
2025-04-19 07:21:19 -07:00
// disable when input is inactive (e.g., overlay open)
isActive={active}
/>
);
}
return (
<Box flexDirection="column">
<Box borderStyle="round">
{loading ? (
<TerminalChatInputThinking
onInterrupt={interruptAgent}
active={active}
thinkingSeconds={thinkingSeconds}
/>
) : (
<Box paddingX={1}>
<TextInput
focus={active}
placeholder={
selectedSuggestion
? `"${suggestions[selectedSuggestion - 1]}"`
: "send a message" +
(isNew ? " or press tab to select a suggestion" : "")
}
showCursor
value={input}
onChange={(value) => {
setDraftInput(value);
if (historyIndex != null) {
setHistoryIndex(null);
}
setInput(value);
}}
onSubmit={onSubmit}
/>
</Box>
)}
</Box>
feat: add /command autocomplete (#317) Add interactive slash‑command autocomplete & navigation in chat input Description This PR enhances the chat input component by adding first‑class support for slash commands (/help, /clear, /compact, etc.) with: * **Live filtering:** As soon as the user types leading `/`, a list of matching commands is shown below the prompt. * **Arrow‑key navigation:** Up/Down arrows cycle through suggestions. * **Enter to autocomplete:** Pressing Enter on a partial command will fill it (without submitting) so you can add arguments or simply press Enter again to execute. * **Type‑safe registry:** A new `slash‑commands.ts` file declares all supported commands in one place, along with TypeScript types to prevent drift. * **Validation:** Only registered commands will ever autocomplete or be suggested; unknown single‑word slash inputs still show an “Invalid command” system message. * **Automated tests:** * Unit tests for the command registry and prefix filtering * Existing tests continue passing with no regressions Motivation Slash commands provide a quick, discoverable way to control the agent (clearing history, compacting context, opening overlays, etc.). Before, users had to memorize the exact command or rely on the generic /help list—autocomplete makes them far more accessible and reduces typos. Changes * `src/utils/slash‑commands.ts` – defines `SlashCommand` and exports a flat list of supported commands + descriptions * `terminal‑chat‑input.tsx` * Import and type the command registry * Render filtered suggestions under the prompt when input starts with `/` * Hook into `useInput` to handle Up/Down and Enter for selection & fill * Flag to swallow the first Enter (autocomplete) and only submit on the next * Updated tests in `tests/slash‑commands.test.ts` to cover registry contents and filtering logic * Removed old JS version and fixed stray `@ts‑expect‑error` How to test locally 1. Type `/` in the prompt—you should see matching commands. 2. Use arrows to move the highlight, press Enter to fill, then Enter again to execute. 3. Run the full test suite (`npm test`) to verify no regressions. Notes * Future work could include fuzzy matching, paging long lists, or more visual styling. * This change is purely additive and does not affect non‑slash inputs or existing slash handlers. --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-20 00:25:46 +10:00
{/* Slash command autocomplete suggestions */}
{input.trim().startsWith("/") && (
<Box flexDirection="column" paddingX={2} marginBottom={1}>
{SLASH_COMMANDS.filter((cmd: SlashCommand) =>
cmd.command.startsWith(input.trim()),
).map((cmd: SlashCommand, idx: number) => (
<Box key={cmd.command}>
<Text
backgroundColor={
idx === selectedSlashSuggestion ? "blackBright" : undefined
}
>
<Text color="blueBright">{cmd.command}</Text>
<Text> {cmd.description}</Text>
</Text>
</Box>
))}
</Box>
)}
<Box paddingX={2} marginBottom={1}>
<Text dimColor>
{isNew && !input ? (
<>
try:{" "}
{suggestions.map((m, key) => (
<Fragment key={key}>
{key !== 0 ? " | " : ""}
<Text
backgroundColor={
key + 1 === selectedSuggestion ? "blackBright" : ""
}
>
{m}
</Text>
</Fragment>
))}
</>
) : (
<>
send q or ctrl+c to exit | send "/clear" to reset | send "/help"
for commands | press enter to send
{contextLeftPercent > 25 && (
<>
{" — "}
<Text color={contextLeftPercent > 40 ? "green" : "yellow"}>
{Math.round(contextLeftPercent)}% context left
</Text>
</>
)}
{contextLeftPercent <= 25 && (
<>
{" — "}
<Text color="red">
{Math.round(contextLeftPercent)}% context left send
"/compact" to condense context
</Text>
</>
)}
</>
)}
</Text>
</Box>
</Box>
);
}
function TerminalChatInputThinking({
onInterrupt,
active,
thinkingSeconds,
}: {
onInterrupt: () => void;
active: boolean;
thinkingSeconds: number;
}) {
const [awaitingConfirm, setAwaitingConfirm] = useState(false);
const [dots, setDots] = useState("");
// Animate ellipsis
useInterval(() => {
setDots((prev) => (prev.length < 3 ? prev + "." : ""));
}, 500);
// Spinner frames with embedded seconds
const ballFrames = [
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"( ●)",
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"(● )",
];
const [frame, setFrame] = useState(0);
useInterval(() => {
setFrame((idx) => (idx + 1) % ballFrames.length);
}, 80);
// Keep the elapsedseconds text fixed while the ball animation moves.
const frameTemplate = ballFrames[frame] ?? ballFrames[0];
const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`;
// ---------------------------------------------------------------------
// Raw stdin listener to catch the case where the terminal delivers two
// consecutive ESC bytes ("\x1B\x1B") in a *single* chunk. Ink's `useInput`
// collapses that sequence into one key event, so the regular twostep
// handler above never sees the second press. By inspecting the raw data
// we can identify this special case and trigger the interrupt while still
// requiring a double press for the normal singlebyte ESC events.
// ---------------------------------------------------------------------
const { stdin, setRawMode } = useStdin();
React.useEffect(() => {
if (!active) {
return;
}
// Ensure raw mode already enabled by Ink when the component has focus,
// but called defensively in case that assumption ever changes.
setRawMode?.(true);
const onData = (data: Buffer | string) => {
if (awaitingConfirm) {
return; // already awaiting a second explicit press
}
// Handle both Buffer and string forms.
const str = Buffer.isBuffer(data) ? data.toString("utf8") : data;
if (str === "\x1b\x1b") {
// Treat as the first Escape press prompt the user for confirmation.
if (isLoggingEnabled()) {
log(
"raw stdin: received collapsed ESC ESC starting confirmation timer",
);
}
setAwaitingConfirm(true);
setTimeout(() => setAwaitingConfirm(false), 1500);
}
};
stdin?.on("data", onData);
return () => {
stdin?.off("data", onData);
};
}, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]);
// No local timer: the parent component supplies the elapsed time via props.
// Listen for the escape key to allow the user to interrupt the current
// operation. We require two presses within a short window (1.5s) to avoid
// accidental cancellations.
useInput(
(_input, key) => {
if (!key.escape) {
return;
}
if (awaitingConfirm) {
if (isLoggingEnabled()) {
log("useInput: second ESC detected triggering onInterrupt()");
}
onInterrupt();
setAwaitingConfirm(false);
} else {
if (isLoggingEnabled()) {
log("useInput: first ESC detected waiting for confirmation");
}
setAwaitingConfirm(true);
setTimeout(() => setAwaitingConfirm(false), 1500);
}
},
{ isActive: active },
);
return (
<Box flexDirection="column" gap={1}>
<Box gap={2}>
<Text>{frameWithSeconds}</Text>
<Text>
Thinking
{dots}
</Text>
</Box>
{awaitingConfirm && (
<Text dimColor>
Press <Text bold>Esc</Text> again to interrupt and enter a new
instruction
</Text>
)}
</Box>
);
}