2025-04-21 01:51:38 +10:00
|
|
|
|
import type { MultilineTextEditorHandle } from "./multiline-editor";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import type { ReviewDecision } from "../../utils/agent/review.js";
|
2025-04-30 19:19:55 -04:00
|
|
|
|
import type { FileSystemSuggestion } from "../../utils/file-system-suggestions.js";
|
2025-04-17 21:41:54 +02:00
|
|
|
|
import type { HistoryEntry } from "../../utils/storage/command-history.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import type {
|
|
|
|
|
|
ResponseInputItem,
|
|
|
|
|
|
ResponseItem,
|
|
|
|
|
|
} from "openai/resources/responses/responses.mjs";
|
|
|
|
|
|
|
2025-04-21 01:51:38 +10:00
|
|
|
|
import MultilineTextEditor from "./multiline-editor";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
|
2025-04-21 00:34:27 -05:00
|
|
|
|
import TextCompletions from "./terminal-chat-completions.js";
|
2025-04-17 21:41:54 +02:00
|
|
|
|
import { loadConfig } from "../../utils/config.js";
|
2025-04-21 00:34:27 -05:00
|
|
|
|
import { getFileSystemSuggestions } from "../../utils/file-system-suggestions.js";
|
2025-04-30 19:19:55 -04:00
|
|
|
|
import { expandFileTags } from "../../utils/file-tag-utils";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { createInputItem } from "../../utils/input-utils.js";
|
2025-04-21 09:51:34 -04:00
|
|
|
|
import { log } from "../../utils/logger/log.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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";
|
2025-04-17 21:41:54 +02:00
|
|
|
|
import {
|
|
|
|
|
|
loadCommandHistory,
|
|
|
|
|
|
addToHistory,
|
|
|
|
|
|
} from "../../utils/storage/command-history.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { clearTerminal, onExit } from "../../utils/terminal.js";
|
|
|
|
|
|
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
|
|
|
|
|
import { fileURLToPath } from "node:url";
|
2025-04-21 01:51:38 +10:00
|
|
|
|
import React, {
|
|
|
|
|
|
useCallback,
|
|
|
|
|
|
useState,
|
|
|
|
|
|
Fragment,
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
useRef,
|
|
|
|
|
|
} from "react";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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,
|
2025-04-18 06:28:58 +10:00
|
|
|
|
explanation,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
submitConfirmation,
|
|
|
|
|
|
setLastResponseId,
|
|
|
|
|
|
setItems,
|
|
|
|
|
|
contextLeftPercent,
|
|
|
|
|
|
openOverlay,
|
|
|
|
|
|
openModelOverlay,
|
|
|
|
|
|
openApprovalOverlay,
|
|
|
|
|
|
openHelpOverlay,
|
2025-04-19 16:23:27 -07:00
|
|
|
|
openDiffOverlay,
|
2025-05-16 12:28:22 -07:00
|
|
|
|
openSessionsOverlay,
|
2025-04-18 15:48:30 +10:00
|
|
|
|
onCompact,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
interruptAgent,
|
|
|
|
|
|
active,
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds,
|
2025-04-18 14:09:35 -07:00
|
|
|
|
items = [],
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}: {
|
|
|
|
|
|
isNew: boolean;
|
|
|
|
|
|
loading: boolean;
|
|
|
|
|
|
submitInput: (input: Array<ResponseInputItem>) => void;
|
|
|
|
|
|
confirmationPrompt: React.ReactNode | null;
|
2025-04-18 06:28:58 +10:00
|
|
|
|
explanation?: string;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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;
|
2025-04-19 16:23:27 -07:00
|
|
|
|
openDiffOverlay: () => void;
|
2025-05-16 12:28:22 -07:00
|
|
|
|
openSessionsOverlay: () => void;
|
2025-04-18 15:48:30 +10:00
|
|
|
|
onCompact: () => void;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
interruptAgent: () => void;
|
|
|
|
|
|
active: boolean;
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds: number;
|
2025-04-18 14:09:35 -07:00
|
|
|
|
// New: current conversation items so we can include them in bug reports
|
|
|
|
|
|
items?: Array<ResponseItem>;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}): 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);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
const app = useApp();
|
|
|
|
|
|
const [selectedSuggestion, setSelectedSuggestion] = useState<number>(0);
|
|
|
|
|
|
const [input, setInput] = useState("");
|
2025-04-17 21:41:54 +02:00
|
|
|
|
const [history, setHistory] = useState<Array<HistoryEntry>>([]);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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);
|
2025-04-30 19:19:55 -04:00
|
|
|
|
const [fsSuggestions, setFsSuggestions] = useState<
|
|
|
|
|
|
Array<FileSystemSuggestion>
|
|
|
|
|
|
>([]);
|
2025-04-21 00:34:27 -05:00
|
|
|
|
const [selectedCompletion, setSelectedCompletion] = useState<number>(-1);
|
2025-04-21 01:51:38 +10:00
|
|
|
|
// Multiline text editor key to force remount after submission
|
2025-04-30 19:19:55 -04:00
|
|
|
|
const [editorState, setEditorState] = useState<{
|
|
|
|
|
|
key: number;
|
|
|
|
|
|
initialCursorOffset?: number;
|
|
|
|
|
|
}>({ key: 0 });
|
2025-04-21 01:51:38 +10:00
|
|
|
|
// Imperative handle from the multiline editor so we can query caret position
|
|
|
|
|
|
const editorRef = useRef<MultilineTextEditorHandle | null>(null);
|
|
|
|
|
|
// Track the caret row across keystrokes
|
|
|
|
|
|
const prevCursorRow = useRef<number | null>(null);
|
2025-04-25 16:11:16 -07:00
|
|
|
|
const prevCursorWasAtLastRow = useRef<boolean>(false);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-30 19:19:55 -04:00
|
|
|
|
// --- Helper for updating input, remounting editor, and moving cursor to end ---
|
|
|
|
|
|
const applyFsSuggestion = useCallback((newInputText: string) => {
|
|
|
|
|
|
setInput(newInputText);
|
|
|
|
|
|
setEditorState((s) => ({
|
|
|
|
|
|
key: s.key + 1,
|
|
|
|
|
|
initialCursorOffset: newInputText.length,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// --- Helper for updating file system suggestions ---
|
|
|
|
|
|
function updateFsSuggestions(
|
|
|
|
|
|
txt: string,
|
|
|
|
|
|
alwaysUpdateSelection: boolean = false,
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Clear file system completions if a space is typed
|
|
|
|
|
|
if (txt.endsWith(" ")) {
|
|
|
|
|
|
setFsSuggestions([]);
|
|
|
|
|
|
setSelectedCompletion(-1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Determine the current token (last whitespace-separated word)
|
|
|
|
|
|
const words = txt.trim().split(/\s+/);
|
|
|
|
|
|
const lastWord = words[words.length - 1] ?? "";
|
|
|
|
|
|
|
|
|
|
|
|
const shouldUpdateSelection =
|
|
|
|
|
|
lastWord.startsWith("@") || alwaysUpdateSelection;
|
|
|
|
|
|
|
|
|
|
|
|
// Strip optional leading '@' for the path prefix
|
|
|
|
|
|
let pathPrefix: string;
|
|
|
|
|
|
if (lastWord.startsWith("@")) {
|
|
|
|
|
|
pathPrefix = lastWord.slice(1);
|
|
|
|
|
|
// If only '@' is typed, list everything in the current directory
|
|
|
|
|
|
pathPrefix = pathPrefix.length === 0 ? "./" : pathPrefix;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pathPrefix = lastWord;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldUpdateSelection) {
|
|
|
|
|
|
const completions = getFileSystemSuggestions(pathPrefix);
|
|
|
|
|
|
setFsSuggestions(completions);
|
|
|
|
|
|
if (completions.length > 0) {
|
|
|
|
|
|
setSelectedCompletion((prev) =>
|
|
|
|
|
|
prev < 0 || prev >= completions.length ? 0 : prev,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedCompletion(-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (fsSuggestions.length > 0) {
|
|
|
|
|
|
// Token cleared → clear menu
|
|
|
|
|
|
setFsSuggestions([]);
|
|
|
|
|
|
setSelectedCompletion(-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Result of replacing text with a file system suggestion
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface ReplacementResult {
|
|
|
|
|
|
/** The new text with the suggestion applied */
|
|
|
|
|
|
text: string;
|
|
|
|
|
|
/** The selected suggestion if a replacement was made */
|
|
|
|
|
|
suggestion: FileSystemSuggestion | null;
|
|
|
|
|
|
/** Whether a replacement was actually made */
|
|
|
|
|
|
wasReplaced: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Helper for replacing input with file system suggestion ---
|
|
|
|
|
|
function getFileSystemSuggestion(
|
|
|
|
|
|
txt: string,
|
|
|
|
|
|
requireAtPrefix: boolean = false,
|
|
|
|
|
|
): ReplacementResult {
|
|
|
|
|
|
if (fsSuggestions.length === 0 || selectedCompletion < 0) {
|
|
|
|
|
|
return { text: txt, suggestion: null, wasReplaced: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const words = txt.trim().split(/\s+/);
|
|
|
|
|
|
const lastWord = words[words.length - 1] ?? "";
|
|
|
|
|
|
|
|
|
|
|
|
// Check if @ prefix is required and the last word doesn't have it
|
|
|
|
|
|
if (requireAtPrefix && !lastWord.startsWith("@")) {
|
|
|
|
|
|
return { text: txt, suggestion: null, wasReplaced: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selected = fsSuggestions[selectedCompletion];
|
|
|
|
|
|
if (!selected) {
|
|
|
|
|
|
return { text: txt, suggestion: null, wasReplaced: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const replacement = lastWord.startsWith("@")
|
|
|
|
|
|
? `@${selected.path}`
|
|
|
|
|
|
: selected.path;
|
|
|
|
|
|
words[words.length - 1] = replacement;
|
|
|
|
|
|
return {
|
|
|
|
|
|
text: words.join(" "),
|
|
|
|
|
|
suggestion: selected,
|
|
|
|
|
|
wasReplaced: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-17 21:41:54 +02:00
|
|
|
|
// 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]);
|
2025-04-17 21:41:54 +02:00
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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
|
2025-04-25 22:21:50 +08:00
|
|
|
|
? 0
|
|
|
|
|
|
: selectedSlashSuggestion + 1;
|
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
|
|
|
|
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;
|
2025-05-16 12:28:22 -07:00
|
|
|
|
case "/sessions":
|
|
|
|
|
|
openSessionsOverlay();
|
|
|
|
|
|
break;
|
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
|
|
|
|
case "/help":
|
|
|
|
|
|
openHelpOverlay();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "/compact":
|
|
|
|
|
|
onCompact();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "/model":
|
|
|
|
|
|
openModelOverlay();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "/approval":
|
|
|
|
|
|
openApprovalOverlay();
|
|
|
|
|
|
break;
|
2025-04-20 13:01:40 +10:00
|
|
|
|
case "/diff":
|
|
|
|
|
|
openDiffOverlay();
|
|
|
|
|
|
break;
|
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
|
|
|
|
case "/bug":
|
|
|
|
|
|
onSubmit(cmd);
|
|
|
|
|
|
break;
|
2025-04-21 12:39:46 -04:00
|
|
|
|
case "/clear":
|
|
|
|
|
|
onSubmit(cmd);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "/clearhistory":
|
|
|
|
|
|
onSubmit(cmd);
|
|
|
|
|
|
break;
|
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
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (!confirmationPrompt && !loading) {
|
2025-04-21 00:34:27 -05:00
|
|
|
|
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) {
|
2025-04-30 19:19:55 -04:00
|
|
|
|
const { text: newText, wasReplaced } =
|
|
|
|
|
|
getFileSystemSuggestion(input);
|
2025-04-21 00:34:27 -05:00
|
|
|
|
|
2025-04-30 19:19:55 -04:00
|
|
|
|
// Only proceed if the text was actually changed
|
|
|
|
|
|
if (wasReplaced) {
|
|
|
|
|
|
applyFsSuggestion(newText);
|
2025-04-21 00:34:27 -05:00
|
|
|
|
setFsSuggestions([]);
|
|
|
|
|
|
setSelectedCompletion(-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (_key.upArrow) {
|
2025-04-25 09:39:24 -07:00
|
|
|
|
let moveThroughHistory = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Only use history when the caret was *already* on the very first
|
2025-04-21 01:51:38 +10:00
|
|
|
|
// row *before* this key-press.
|
|
|
|
|
|
const cursorRow = editorRef.current?.getRow?.() ?? 0;
|
2025-04-25 16:11:16 -07:00
|
|
|
|
const cursorCol = editorRef.current?.getCol?.() ?? 0;
|
2025-04-21 01:51:38 +10:00
|
|
|
|
const wasAtFirstRow = (prevCursorRow.current ?? cursorRow) === 0;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
if (!(cursorRow === 0 && wasAtFirstRow)) {
|
|
|
|
|
|
moveThroughHistory = false;
|
|
|
|
|
|
}
|
2025-04-21 01:51:38 +10:00
|
|
|
|
|
2025-04-25 16:11:16 -07:00
|
|
|
|
// If we are not yet in history mode, then also require that the col is zero so that
|
|
|
|
|
|
// we only trigger history navigation when the user is at the start of the input.
|
|
|
|
|
|
if (historyIndex == null && !(cursorRow === 0 && cursorCol === 0)) {
|
2025-04-25 09:39:24 -07:00
|
|
|
|
moveThroughHistory = false;
|
|
|
|
|
|
}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// Move through history.
|
|
|
|
|
|
if (history.length && moveThroughHistory) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
let newIndex: number;
|
|
|
|
|
|
if (historyIndex == null) {
|
2025-04-25 09:39:24 -07:00
|
|
|
|
const currentDraft = editorRef.current?.getText?.() ?? input;
|
|
|
|
|
|
setDraftInput(currentDraft);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
newIndex = history.length - 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newIndex = Math.max(0, historyIndex - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
setHistoryIndex(newIndex);
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
2025-04-17 21:41:54 +02:00
|
|
|
|
setInput(history[newIndex]?.command ?? "");
|
2025-04-21 01:51:38 +10:00
|
|
|
|
// Re-mount the editor so it picks up the new initialText
|
2025-04-30 19:19:55 -04:00
|
|
|
|
setEditorState((s) => ({ key: s.key + 1 }));
|
2025-04-25 09:39:24 -07:00
|
|
|
|
return; // handled
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
|
|
|
|
|
// Otherwise let it propagate.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_key.downArrow) {
|
2025-04-21 01:51:38 +10:00
|
|
|
|
// Only move forward in history when we're already *in* history mode
|
2025-04-25 16:11:16 -07:00
|
|
|
|
// AND the caret sits on the last line of the buffer.
|
|
|
|
|
|
const wasAtLastRow =
|
|
|
|
|
|
prevCursorWasAtLastRow.current ??
|
|
|
|
|
|
editorRef.current?.isCursorAtLastRow() ??
|
|
|
|
|
|
true;
|
|
|
|
|
|
if (historyIndex != null && wasAtLastRow) {
|
2025-04-21 01:51:38 +10:00
|
|
|
|
const newIndex = historyIndex + 1;
|
|
|
|
|
|
if (newIndex >= history.length) {
|
|
|
|
|
|
setHistoryIndex(null);
|
|
|
|
|
|
setInput(draftInput);
|
2025-04-30 19:19:55 -04:00
|
|
|
|
setEditorState((s) => ({ key: s.key + 1 }));
|
2025-04-21 01:51:38 +10:00
|
|
|
|
} else {
|
|
|
|
|
|
setHistoryIndex(newIndex);
|
|
|
|
|
|
setInput(history[newIndex]?.command ?? "");
|
2025-04-30 19:19:55 -04:00
|
|
|
|
setEditorState((s) => ({ key: s.key + 1 }));
|
2025-04-21 01:51:38 +10:00
|
|
|
|
}
|
|
|
|
|
|
return; // handled
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
2025-04-21 01:51:38 +10:00
|
|
|
|
// Otherwise let it propagate
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
2025-04-21 00:34:27 -05:00
|
|
|
|
|
2025-04-30 19:19:55 -04:00
|
|
|
|
// Defer filesystem suggestion logic to onSubmit if enter key is pressed
|
|
|
|
|
|
if (!_key.return) {
|
|
|
|
|
|
// Pressing tab should trigger the file system suggestions
|
|
|
|
|
|
const shouldUpdateSelection = _key.tab;
|
|
|
|
|
|
const targetInput = _key.delete ? input.slice(0, -1) : input + _input;
|
|
|
|
|
|
updateFsSuggestions(targetInput, shouldUpdateSelection);
|
2025-04-21 00:34:27 -05:00
|
|
|
|
}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-25 16:11:16 -07:00
|
|
|
|
// Update the cached cursor position *after* **all** handlers (including
|
|
|
|
|
|
// the internal <MultilineTextEditor>) have processed this key event.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Ink invokes `useInput` callbacks starting with **parent** components
|
|
|
|
|
|
// first, followed by their descendants. As a result the call above
|
|
|
|
|
|
// executes *before* the editor has had a chance to react to the key
|
|
|
|
|
|
// press and update its internal caret position. When navigating
|
|
|
|
|
|
// through a multi-line draft with the ↑ / ↓ arrow keys this meant we
|
|
|
|
|
|
// recorded the *old* cursor row instead of the one that results *after*
|
|
|
|
|
|
// the key press. Consequently, a subsequent ↑ still saw
|
|
|
|
|
|
// `prevCursorRow = 1` even though the caret was already on row 0 and
|
|
|
|
|
|
// history-navigation never kicked in.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Defer the sampling by one tick so we read the *final* caret position
|
|
|
|
|
|
// for this frame.
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
prevCursorRow.current = editorRef.current?.getRow?.() ?? null;
|
|
|
|
|
|
prevCursorWasAtLastRow.current =
|
|
|
|
|
|
editorRef.current?.isCursorAtLastRow?.() ?? true;
|
|
|
|
|
|
}, 1);
|
2025-04-21 01:51:38 +10:00
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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();
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
|
|
|
|
|
// If the user only entered a slash, do not send a chat message.
|
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 (inputValue === "/") {
|
|
|
|
|
|
setInput("");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
|
|
|
|
|
// Skip this submit if we just autocompleted a slash command.
|
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 (skipNextSubmit) {
|
|
|
|
|
|
setSkipNextSubmit(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (!inputValue) {
|
|
|
|
|
|
return;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
} else if (inputValue === "/history") {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setInput("");
|
|
|
|
|
|
openOverlay();
|
|
|
|
|
|
return;
|
2025-05-16 12:28:22 -07:00
|
|
|
|
} else if (inputValue === "/sessions") {
|
|
|
|
|
|
setInput("");
|
|
|
|
|
|
openSessionsOverlay();
|
|
|
|
|
|
return;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
} else if (inputValue === "/help") {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setInput("");
|
|
|
|
|
|
openHelpOverlay();
|
|
|
|
|
|
return;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
} else if (inputValue === "/diff") {
|
2025-04-19 16:23:27 -07:00
|
|
|
|
setInput("");
|
|
|
|
|
|
openDiffOverlay();
|
|
|
|
|
|
return;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
} else if (inputValue === "/compact") {
|
2025-04-18 15:48:30 +10:00
|
|
|
|
setInput("");
|
|
|
|
|
|
onCompact();
|
|
|
|
|
|
return;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
} else if (inputValue.startsWith("/model")) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setInput("");
|
|
|
|
|
|
openModelOverlay();
|
|
|
|
|
|
return;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
} else if (inputValue.startsWith("/approval")) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setInput("");
|
|
|
|
|
|
openApprovalOverlay();
|
|
|
|
|
|
return;
|
2025-04-25 16:58:09 -07:00
|
|
|
|
} else if (["exit", "q", ":q"].includes(inputValue)) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setInput("");
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
app.exit();
|
|
|
|
|
|
onExit();
|
|
|
|
|
|
process.exit(0);
|
2025-04-25 09:39:24 -07:00
|
|
|
|
}, 60); // Wait one frame.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
return;
|
|
|
|
|
|
} else if (inputValue === "/clear" || inputValue === "clear") {
|
|
|
|
|
|
setInput("");
|
|
|
|
|
|
setSessionId("");
|
|
|
|
|
|
setLastResponseId("");
|
|
|
|
|
|
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// Clear the terminal screen (including scrollback) before resetting context.
|
|
|
|
|
|
clearTerminal();
|
2025-04-21 12:39:46 -04:00
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// 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.
|
2025-04-20 08:52:14 -07:00
|
|
|
|
setItems((prev) => {
|
|
|
|
|
|
const filteredOldItems = prev.filter((item) => {
|
2025-04-21 12:39:46 -04:00
|
|
|
|
// Remove any token‑heavy entries (user/assistant turns and function calls)
|
2025-04-20 08:52:14 -07:00
|
|
|
|
if (
|
|
|
|
|
|
item.type === "message" &&
|
|
|
|
|
|
(item.role === "user" || item.role === "assistant")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-04-21 12:39:46 -04:00
|
|
|
|
if (
|
|
|
|
|
|
item.type === "function_call" ||
|
|
|
|
|
|
item.type === "function_call_output"
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true; // keep developer/system and other meta entries
|
2025-04-20 08:52:14 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
...filteredOldItems,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: `clear-${Date.now()}`,
|
|
|
|
|
|
type: "message",
|
|
|
|
|
|
role: "system",
|
|
|
|
|
|
content: [{ type: "input_text", text: "Terminal cleared" }],
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-17 21:41:54 +02:00
|
|
|
|
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([]);
|
|
|
|
|
|
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// Emit a system message to confirm the history clear action.
|
2025-04-17 21:41:54 +02:00
|
|
|
|
setItems((prev) => [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: `clearhistory-${Date.now()}`,
|
|
|
|
|
|
type: "message",
|
|
|
|
|
|
role: "system",
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{ type: "input_text", text: "Command history cleared" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-04-18 14:09:35 -07:00
|
|
|
|
return;
|
|
|
|
|
|
} else if (inputValue === "/bug") {
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// Generate a GitHub bug report URL pre‑filled with session details.
|
2025-04-18 14:09:35 -07:00
|
|
|
|
setInput("");
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-04-25 05:30:14 +05:30
|
|
|
|
const os = await import("node:os");
|
fix: always load version from package.json at runtime (#909)
Note the high-level motivation behind this change is to avoid the need
to make temporary changes in the source tree in order to cut a release
build since that runs the risk of leaving things in an inconsistent
state in the event of a failure. The existing code:
```
import pkg from "../../package.json" assert { type: "json" };
```
did not work as intended because, as written, ESBuild would bake the
contents of the local `package.json` into the release build at build
time whereas we want it to read the contents at runtime so we can use
the `package.json` in the tree to build the code and later inject a
modified version into the release package with a timestamped build
version.
Changes:
* move `CLI_VERSION` out of `src/utils/session.ts` and into
`src/version.ts` so `../package.json` is a correct relative path both
from `src/version.ts` in the source tree and also in the final
`dist/cli.js` build output
* change `assert` to `with` in `import pkg` as apparently `with` became
standard in Node 22
* mark `"../package.json"` as external in `build.mjs` so the version is
not baked into the `.js` at build time
After using `pnpm stage-release` to build a release version, if I use
Node 22.0 to run Codex, I see the following printed to stderr at
startup:
```
(node:71308) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
```
Note it is a warning and does not prevent Codex from running.
In Node 22.12, the warning goes away, but the warning still appears in
Node 22.11. For Node 22, 22.15.0 is the current LTS version, so LTS
users will not see this.
Also, something about moving the definition of `CLI_VERSION` caused a
problem with the mocks in `check-updates.test.ts`. I asked Codex to fix
it, and it came up with the change to the test configs. I don't know
enough about vitest to understand what it did, but the tests seem
healthy again, so I'm going with it.
2025-05-12 21:27:15 -07:00
|
|
|
|
const { CLI_VERSION } = await import("../../version.js");
|
2025-04-18 14:09:35 -07:00
|
|
|
|
const { buildBugReportUrl } = await import(
|
|
|
|
|
|
"../../utils/bug-report.js"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const url = buildBugReportUrl({
|
|
|
|
|
|
items: items ?? [],
|
|
|
|
|
|
cliVersion: CLI_VERSION,
|
|
|
|
|
|
model: loadConfig().model ?? "unknown",
|
2025-04-18 18:13:34 -07:00
|
|
|
|
platform: [os.platform(), os.arch(), os.release()]
|
|
|
|
|
|
.map((s) => `\`${s}\``)
|
|
|
|
|
|
.join(" | "),
|
2025-04-18 14:09:35 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setItems((prev) => [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: `bugreport-${Date.now()}`,
|
|
|
|
|
|
type: "message",
|
|
|
|
|
|
role: "system",
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "input_text",
|
2025-04-25 05:30:14 +05:30
|
|
|
|
text: `🔗 Bug report URL: ${url}`,
|
2025-04-18 14:09:35 -07:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
} catch (error) {
|
2025-04-25 05:30:14 +05:30
|
|
|
|
// If anything went wrong, notify the user.
|
2025-04-18 14:09:35 -07:00
|
|
|
|
setItems((prev) => [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: `bugreport-error-${Date.now()}`,
|
|
|
|
|
|
type: "message",
|
|
|
|
|
|
role: "system",
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "input_text",
|
|
|
|
|
|
text: `⚠️ Failed to create bug report URL: ${error}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
return;
|
2025-04-18 07:50:27 +02:00
|
|
|
|
} else if (inputValue.startsWith("/")) {
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// 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.
|
2025-04-18 07:50:27 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-18 09:17:13 +10:00
|
|
|
|
// detect image file paths for dynamic inclusion
|
2025-04-16 12:56:08 -04:00
|
|
|
|
const images: Array<string> = [];
|
2025-04-18 09:17:13 +10:00
|
|
|
|
let text = inputValue;
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
2025-04-18 09:17:13 +10:00
|
|
|
|
// markdown-style image syntax: 
|
|
|
|
|
|
text = text.replace(/!\[[^\]]*?\]\(([^)]+)\)/g, (_m, p1: string) => {
|
|
|
|
|
|
images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1);
|
|
|
|
|
|
return "";
|
|
|
|
|
|
});
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
2025-04-18 09:17:13 +10:00
|
|
|
|
// 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) => {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1);
|
|
|
|
|
|
return "";
|
2025-04-18 09:17:13 +10:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-04-25 09:39:24 -07:00
|
|
|
|
|
2025-04-18 09:17:13 +10:00
|
|
|
|
// 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();
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-30 19:19:55 -04:00
|
|
|
|
// Expand @file tokens into XML blocks for the model
|
|
|
|
|
|
const expandedText = await expandFileTags(text);
|
|
|
|
|
|
|
|
|
|
|
|
const inputItem = await createInputItem(expandedText, images);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
submitInput([inputItem]);
|
2025-04-17 21:41:54 +02:00
|
|
|
|
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// Get config for history persistence.
|
2025-04-17 21:41:54 +02:00
|
|
|
|
const config = loadConfig();
|
|
|
|
|
|
|
2025-04-25 09:39:24 -07:00
|
|
|
|
// Add to history and update state.
|
2025-04-17 21:41:54 +02:00
|
|
|
|
const updatedHistory = await addToHistory(value, history, {
|
|
|
|
|
|
maxSize: config.history?.maxSize ?? 1000,
|
|
|
|
|
|
saveHistory: config.history?.saveHistory ?? true,
|
|
|
|
|
|
sensitivePatterns: config.history?.sensitivePatterns ?? [],
|
2025-04-16 12:56:08 -04:00
|
|
|
|
});
|
2025-04-17 21:41:54 +02:00
|
|
|
|
|
|
|
|
|
|
setHistory(updatedHistory);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setHistoryIndex(null);
|
|
|
|
|
|
setDraftInput("");
|
|
|
|
|
|
setSelectedSuggestion(0);
|
|
|
|
|
|
setInput("");
|
2025-04-21 00:34:27 -05:00
|
|
|
|
setFsSuggestions([]);
|
|
|
|
|
|
setSelectedCompletion(-1);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
},
|
|
|
|
|
|
[
|
|
|
|
|
|
setInput,
|
|
|
|
|
|
submitInput,
|
|
|
|
|
|
setLastResponseId,
|
|
|
|
|
|
setItems,
|
|
|
|
|
|
app,
|
|
|
|
|
|
setHistory,
|
|
|
|
|
|
setHistoryIndex,
|
|
|
|
|
|
openOverlay,
|
|
|
|
|
|
openApprovalOverlay,
|
|
|
|
|
|
openModelOverlay,
|
|
|
|
|
|
openHelpOverlay,
|
2025-04-19 16:23:27 -07:00
|
|
|
|
openDiffOverlay,
|
2025-05-16 12:28:22 -07:00
|
|
|
|
openSessionsOverlay,
|
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,
|
2025-04-18 15:48:30 +10:00
|
|
|
|
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,
|
2025-04-18 14:09:35 -07:00
|
|
|
|
items,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (confirmationPrompt) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TerminalChatCommandReview
|
|
|
|
|
|
confirmationPrompt={confirmationPrompt}
|
|
|
|
|
|
onReviewCommand={submitConfirmation}
|
2025-04-19 07:21:19 -07:00
|
|
|
|
// allow switching approval mode via 'v'
|
|
|
|
|
|
onSwitchApprovalMode={openApprovalOverlay}
|
2025-04-18 06:28:58 +10:00
|
|
|
|
explanation={explanation}
|
2025-04-19 07:21:19 -07:00
|
|
|
|
// disable when input is inactive (e.g., overlay open)
|
|
|
|
|
|
isActive={active}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box flexDirection="column">
|
|
|
|
|
|
<Box borderStyle="round">
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<TerminalChatInputThinking
|
|
|
|
|
|
onInterrupt={interruptAgent}
|
|
|
|
|
|
active={active}
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds={thinkingSeconds}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
2025-04-21 00:34:27 -05:00
|
|
|
|
<Box paddingX={1}>
|
2025-04-21 01:51:38 +10:00
|
|
|
|
<MultilineTextEditor
|
|
|
|
|
|
ref={editorRef}
|
|
|
|
|
|
onChange={(txt: string) => {
|
|
|
|
|
|
setDraftInput(txt);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (historyIndex != null) {
|
|
|
|
|
|
setHistoryIndex(null);
|
|
|
|
|
|
}
|
2025-04-21 01:51:38 +10:00
|
|
|
|
setInput(txt);
|
|
|
|
|
|
}}
|
2025-04-30 19:19:55 -04:00
|
|
|
|
key={editorState.key}
|
|
|
|
|
|
initialCursorOffset={editorState.initialCursorOffset}
|
2025-04-21 01:51:38 +10:00
|
|
|
|
initialText={input}
|
|
|
|
|
|
height={6}
|
|
|
|
|
|
focus={active}
|
|
|
|
|
|
onSubmit={(txt) => {
|
2025-04-30 19:19:55 -04:00
|
|
|
|
// If final token is an @path, replace with filesystem suggestion if available
|
|
|
|
|
|
const {
|
|
|
|
|
|
text: replacedText,
|
|
|
|
|
|
suggestion,
|
|
|
|
|
|
wasReplaced,
|
|
|
|
|
|
} = getFileSystemSuggestion(txt, true);
|
|
|
|
|
|
|
|
|
|
|
|
// If we replaced @path token with a directory, don't submit
|
|
|
|
|
|
if (wasReplaced && suggestion?.isDirectory) {
|
|
|
|
|
|
applyFsSuggestion(replacedText);
|
|
|
|
|
|
// Update suggestions for the new directory
|
|
|
|
|
|
updateFsSuggestions(replacedText, true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onSubmit(replacedText);
|
|
|
|
|
|
setEditorState((s) => ({ key: s.key + 1 }));
|
2025-04-21 01:51:38 +10:00
|
|
|
|
setInput("");
|
|
|
|
|
|
setHistoryIndex(null);
|
|
|
|
|
|
setDraftInput("");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
<Box paddingX={2} marginBottom={1}>
|
2025-04-21 00:34:27 -05:00
|
|
|
|
{isNew && !input ? (
|
|
|
|
|
|
<Text dimColor>
|
|
|
|
|
|
try:{" "}
|
|
|
|
|
|
{suggestions.map((m, key) => (
|
|
|
|
|
|
<Fragment key={key}>
|
|
|
|
|
|
{key !== 0 ? " | " : ""}
|
|
|
|
|
|
<Text
|
|
|
|
|
|
backgroundColor={
|
|
|
|
|
|
key + 1 === selectedSuggestion ? "blackBright" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{m}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Fragment>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
) : fsSuggestions.length > 0 ? (
|
|
|
|
|
|
<TextCompletions
|
2025-04-30 19:19:55 -04:00
|
|
|
|
completions={fsSuggestions.map((suggestion) => suggestion.path)}
|
2025-04-21 00:34:27 -05:00
|
|
|
|
selectedCompletion={selectedCompletion}
|
|
|
|
|
|
displayLimit={5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Text dimColor>
|
2025-08-07 03:29:33 -07:00
|
|
|
|
Ctrl+C to exit | "/" to see commands | Enter to send
|
2025-04-21 00:34:27 -05:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
</Box>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function TerminalChatInputThinking({
|
|
|
|
|
|
onInterrupt,
|
|
|
|
|
|
active,
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}: {
|
|
|
|
|
|
onInterrupt: () => void;
|
|
|
|
|
|
active: boolean;
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds: number;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}) {
|
|
|
|
|
|
const [awaitingConfirm, setAwaitingConfirm] = useState(false);
|
2025-04-18 18:13:34 -07:00
|
|
|
|
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 elapsed‑seconds text fixed while the ball animation moves.
|
|
|
|
|
|
const frameTemplate = ballFrames[frame] ?? ballFrames[0];
|
|
|
|
|
|
const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
|
// 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 two‑step
|
|
|
|
|
|
// 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 single‑byte 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.
|
2025-04-20 09:58:06 -07:00
|
|
|
|
log(
|
|
|
|
|
|
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
|
|
|
|
|
|
);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setAwaitingConfirm(true);
|
|
|
|
|
|
setTimeout(() => setAwaitingConfirm(false), 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
stdin?.on("data", onData);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
stdin?.off("data", onData);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]);
|
|
|
|
|
|
|
2025-04-18 18:13:34 -07:00
|
|
|
|
// No local timer: the parent component supplies the elapsed time via props.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-04-20 09:58:06 -07:00
|
|
|
|
log("useInput: second ESC detected – triggering onInterrupt()");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
onInterrupt();
|
|
|
|
|
|
setAwaitingConfirm(false);
|
|
|
|
|
|
} else {
|
2025-04-20 09:58:06 -07:00
|
|
|
|
log("useInput: first ESC detected – waiting for confirmation");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
setAwaitingConfirm(true);
|
|
|
|
|
|
setTimeout(() => setAwaitingConfirm(false), 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ isActive: active },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-04-25 16:58:09 -07:00
|
|
|
|
<Box width="100%" flexDirection="column" gap={1}>
|
|
|
|
|
|
<Box
|
|
|
|
|
|
flexDirection="row"
|
|
|
|
|
|
width="100%"
|
|
|
|
|
|
justifyContent="space-between"
|
|
|
|
|
|
paddingRight={1}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Box gap={2}>
|
|
|
|
|
|
<Text>{frameWithSeconds}</Text>
|
|
|
|
|
|
<Text>
|
|
|
|
|
|
Thinking
|
|
|
|
|
|
{dots}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
2025-04-18 18:13:34 -07:00
|
|
|
|
<Text>
|
2025-04-25 16:58:09 -07:00
|
|
|
|
<Text dimColor>press</Text> <Text bold>Esc</Text>{" "}
|
|
|
|
|
|
{awaitingConfirm ? (
|
|
|
|
|
|
<Text bold>again</Text>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Text dimColor>twice</Text>
|
|
|
|
|
|
)}{" "}
|
|
|
|
|
|
<Text dimColor>to interrupt</Text>
|
2025-04-18 18:13:34 -07:00
|
|
|
|
</Text>
|
2025-04-16 12:56:08 -04:00
|
|
|
|
</Box>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|