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-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-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;
|
|
|
|
|
|
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-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");
|
2025-04-18 14:09:35 -07:00
|
|
|
|
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",
|
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,
|
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-04-25 09:39:24 -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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|