Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/update_vercel_docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ cd "$TEMP_DIR"
git init -q
git remote add origin https://github.com/vercel/ai.git
git config core.sparseCheckout true
mkdir -p .git/info
echo "content/*" >.git/info/sparse-checkout

git fetch --depth=1 origin main
Expand Down
47 changes: 38 additions & 9 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { SendMessageError as SendMessageErrorType } from "../types/errors";
import { usePersistedState } from "../hooks/usePersistedState";
import { ThinkingSliderComponent } from "./ThinkingSlider";
import { useThinkingLevel } from "../hooks/useThinkingLevel";
import { getSlashCommandSuggestions, type SlashSuggestion } from "../utils/slashCommands";

const InputSection = styled.div`
position: relative;
Expand Down Expand Up @@ -248,7 +249,8 @@ export const ChatInput: React.FC<ChatInputProps> = ({
const [input, setInput] = usePersistedState("input:" + workspaceId, "");
const [isSending, setIsSending] = useState(false);
const [showCommandSuggestions, setShowCommandSuggestions] = useState(false);
const [availableCommands] = useState<string[]>([]); // Will be populated in future
const [commandSuggestions, setCommandSuggestions] = useState<SlashSuggestion[]>([]);
const [providerNames, setProviderNames] = useState<string[]>([]);
const [toast, setToast] = useState<Toast | null>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [thinkingLevel] = useThinkingLevel();
Expand All @@ -270,13 +272,37 @@ export const ChatInput: React.FC<ChatInputProps> = ({

// Watch input for slash commands
useEffect(() => {
setShowCommandSuggestions(input.startsWith("/") && availableCommands.length > 0);
}, [input, availableCommands]);
const suggestions = getSlashCommandSuggestions(input, { providerNames });
setCommandSuggestions(suggestions);
setShowCommandSuggestions(suggestions.length > 0);
}, [input, providerNames]);

// Load provider names for suggestions
useEffect(() => {
let isMounted = true;

const loadProviders = async () => {
try {
const names = await window.api.providers.list();
if (isMounted && Array.isArray(names)) {
setProviderNames(names);
}
} catch (error) {
console.error("Failed to load provider list:", error);
}
};

void loadProviders();

return () => {
isMounted = false;
};
}, []);

// Handle command selection
const handleCommandSelect = useCallback(
(command: string) => {
setInput(`/${command} `);
(suggestion: SlashSuggestion) => {
setInput(suggestion.replacement);
setShowCommandSuggestions(false);
inputRef.current?.focus();
},
Expand Down Expand Up @@ -404,7 +430,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({
}

// Don't handle keys if command suggestions are visible
if (showCommandSuggestions && COMMAND_SUGGESTION_KEYS.includes(e.key)) {
if (
showCommandSuggestions &&
commandSuggestions.length > 0 &&
COMMAND_SUGGESTION_KEYS.includes(e.key)
) {
return; // Let CommandSuggestions handle it
}

Expand All @@ -424,9 +454,8 @@ export const ChatInput: React.FC<ChatInputProps> = ({
<InputSection>
<ChatInputToast toast={toast} onDismiss={() => setToast(null)} />
<CommandSuggestions
input={input}
availableCommands={availableCommands}
onSelectCommand={handleCommandSelect}
suggestions={commandSuggestions}
onSelectSuggestion={handleCommandSelect}
onDismiss={() => setShowCommandSuggestions(false)}
isVisible={showCommandSuggestions}
/>
Expand Down
65 changes: 20 additions & 45 deletions src/components/CommandSuggestions.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useState, useEffect } from "react";
import styled from "@emotion/styled";
import type { SlashSuggestion } from "../utils/slashCommands";

// Export the keys that CommandSuggestions handles
export const COMMAND_SUGGESTION_KEYS = ["Tab", "ArrowUp", "ArrowDown", "Escape"];

// Props interface
interface CommandSuggestionsProps {
input: string;
availableCommands: string[];
onSelectCommand: (command: string) => void;
suggestions: SlashSuggestion[];
onSelectSuggestion: (suggestion: SlashSuggestion) => void;
onDismiss: () => void;
isVisible: boolean;
}
Expand Down Expand Up @@ -79,61 +79,36 @@ const HelperText = styled.div`

// Main component
export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({
input,
availableCommands,
onSelectCommand,
suggestions,
onSelectSuggestion,
onDismiss,
isVisible,
}) => {
const [filteredCommands, setFilteredCommands] = useState<string[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);

// Command descriptions for built-in commands
const getDescription = (cmd: string): string => {
const descriptions: Record<string, string> = {
clear: "Clear conversation and start fresh",
compact: "Compress conversation history",
context: "Show context usage information",
cost: "Show token usage and costs",
init: "Initialize or reinitialize session",
model: "Switch AI model",
help: "Show available commands",
};

return descriptions[cmd] || `/${cmd}`;
};

// Filter commands based on input
// Reset selection whenever suggestions change
useEffect(() => {
if (input.startsWith("/")) {
const searchTerm = input.slice(1).toLowerCase();
const filtered = availableCommands
.filter((cmd) => cmd.toLowerCase().startsWith(searchTerm))
.slice(0, 10);

setFilteredCommands(filtered);
setSelectedIndex(0);
}
}, [input, availableCommands]);
setSelectedIndex(0);
}, [suggestions]);

// Handle keyboard navigation
useEffect(() => {
if (!isVisible) return;
if (!isVisible || suggestions.length === 0) return;

const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setSelectedIndex((i) => (i + 1) % filteredCommands.length);
setSelectedIndex((i) => (i + 1) % suggestions.length);
break;
case "ArrowUp":
e.preventDefault();
setSelectedIndex((i) => (i - 1 + filteredCommands.length) % filteredCommands.length);
setSelectedIndex((i) => (i - 1 + suggestions.length) % suggestions.length);
break;
case "Tab":
if (!e.shiftKey && filteredCommands.length > 0) {
if (!e.shiftKey && suggestions.length > 0) {
e.preventDefault();
onSelectCommand(filteredCommands[selectedIndex]);
onSelectSuggestion(suggestions[selectedIndex]);
}
break;
case "Escape":
Expand All @@ -145,7 +120,7 @@ export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isVisible, filteredCommands, selectedIndex, onSelectCommand, onDismiss]);
}, [isVisible, suggestions, selectedIndex, onSelectSuggestion, onDismiss]);

// Click outside handler
useEffect(() => {
Expand All @@ -162,21 +137,21 @@ export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isVisible, onDismiss]);

if (!isVisible || filteredCommands.length === 0) {
if (!isVisible || suggestions.length === 0) {
return null;
}

return (
<PopoverContainer data-command-suggestions>
{filteredCommands.map((cmd, index) => (
{suggestions.map((suggestion, index) => (
<CommandItem
key={cmd}
key={suggestion.id}
selected={index === selectedIndex}
onMouseEnter={() => setSelectedIndex(index)}
onClick={() => onSelectCommand(cmd)}
onClick={() => onSelectSuggestion(suggestion)}
>
<CommandText>/{cmd}</CommandText>
<CommandDescription>{getDescription(cmd)}</CommandDescription>
<CommandText>{suggestion.display}</CommandText>
<CommandDescription>{suggestion.description}</CommandDescription>
</CommandItem>
))}
<HelperText>
Expand Down
Loading