mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feature(commands) - Refactor Slash Command + Vision For the Future (#3175)
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
MAX_SUGGESTIONS_TO_SHOW,
|
||||
Suggestion,
|
||||
} from '../components/SuggestionsDisplay.js';
|
||||
import { SlashCommand } from './slashCommandProcessor.js';
|
||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
|
||||
export interface UseCompletionReturn {
|
||||
suggestions: Suggestion[];
|
||||
@@ -40,6 +40,7 @@ export function useCompletion(
|
||||
cwd: string,
|
||||
isActive: boolean,
|
||||
slashCommands: SlashCommand[],
|
||||
commandContext: CommandContext,
|
||||
config?: Config,
|
||||
): UseCompletionReturn {
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
||||
@@ -123,75 +124,129 @@ export function useCompletion(
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedQuery = query.trimStart(); // Trim leading whitespace
|
||||
const trimmedQuery = query.trimStart();
|
||||
|
||||
// --- Handle Slash Command Completion ---
|
||||
if (trimmedQuery.startsWith('/')) {
|
||||
const parts = trimmedQuery.substring(1).split(' ');
|
||||
const commandName = parts[0];
|
||||
const subCommand = parts.slice(1).join(' ');
|
||||
const fullPath = trimmedQuery.substring(1);
|
||||
const hasTrailingSpace = trimmedQuery.endsWith(' ');
|
||||
|
||||
const command = slashCommands.find(
|
||||
(cmd) => cmd.name === commandName || cmd.altName === commandName,
|
||||
);
|
||||
// Get all non-empty parts of the command.
|
||||
const rawParts = fullPath.split(/\s+/).filter((p) => p);
|
||||
|
||||
// Continue to show command help until user types past command name.
|
||||
if (command && command.completion && parts.length > 1) {
|
||||
let commandPathParts = rawParts;
|
||||
let partial = '';
|
||||
|
||||
// If there's no trailing space, the last part is potentially a partial segment.
|
||||
// We tentatively separate it.
|
||||
if (!hasTrailingSpace && rawParts.length > 0) {
|
||||
partial = rawParts[rawParts.length - 1];
|
||||
commandPathParts = rawParts.slice(0, -1);
|
||||
}
|
||||
|
||||
// Traverse the Command Tree using the tentative completed path
|
||||
let currentLevel: SlashCommand[] | undefined = slashCommands;
|
||||
let leafCommand: SlashCommand | null = null;
|
||||
|
||||
for (const part of commandPathParts) {
|
||||
if (!currentLevel) {
|
||||
leafCommand = null;
|
||||
currentLevel = [];
|
||||
break;
|
||||
}
|
||||
const found: SlashCommand | undefined = currentLevel.find(
|
||||
(cmd) => cmd.name === part || cmd.altName === part,
|
||||
);
|
||||
if (found) {
|
||||
leafCommand = found;
|
||||
currentLevel = found.subCommands;
|
||||
} else {
|
||||
leafCommand = null;
|
||||
currentLevel = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the Ambiguous Case
|
||||
if (!hasTrailingSpace && currentLevel) {
|
||||
const exactMatchAsParent = currentLevel.find(
|
||||
(cmd) =>
|
||||
(cmd.name === partial || cmd.altName === partial) &&
|
||||
cmd.subCommands,
|
||||
);
|
||||
|
||||
if (exactMatchAsParent) {
|
||||
// It's a perfect match for a parent command. Override our initial guess.
|
||||
// Treat it as a completed command path.
|
||||
leafCommand = exactMatchAsParent;
|
||||
currentLevel = exactMatchAsParent.subCommands;
|
||||
partial = ''; // We now want to suggest ALL of its sub-commands.
|
||||
}
|
||||
}
|
||||
|
||||
const depth = commandPathParts.length;
|
||||
|
||||
// Provide Suggestions based on the now-corrected context
|
||||
|
||||
// Argument Completion
|
||||
if (
|
||||
leafCommand?.completion &&
|
||||
(hasTrailingSpace ||
|
||||
(rawParts.length > depth && depth > 0 && partial !== ''))
|
||||
) {
|
||||
const fetchAndSetSuggestions = async () => {
|
||||
setIsLoadingSuggestions(true);
|
||||
if (command.completion) {
|
||||
const results = await command.completion();
|
||||
const filtered = results.filter((r) => r.startsWith(subCommand));
|
||||
const newSuggestions = filtered.map((s) => ({
|
||||
label: s,
|
||||
value: s,
|
||||
}));
|
||||
setSuggestions(newSuggestions);
|
||||
setShowSuggestions(newSuggestions.length > 0);
|
||||
setActiveSuggestionIndex(newSuggestions.length > 0 ? 0 : -1);
|
||||
}
|
||||
const argString = rawParts.slice(depth).join(' ');
|
||||
const results =
|
||||
(await leafCommand!.completion!(commandContext, argString)) || [];
|
||||
const finalSuggestions = results.map((s) => ({ label: s, value: s }));
|
||||
setSuggestions(finalSuggestions);
|
||||
setShowSuggestions(finalSuggestions.length > 0);
|
||||
setActiveSuggestionIndex(finalSuggestions.length > 0 ? 0 : -1);
|
||||
setIsLoadingSuggestions(false);
|
||||
};
|
||||
fetchAndSetSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
const partialCommand = trimmedQuery.substring(1);
|
||||
const filteredSuggestions = slashCommands
|
||||
.filter(
|
||||
// Command/Sub-command Completion
|
||||
const commandsToSearch = currentLevel || [];
|
||||
if (commandsToSearch.length > 0) {
|
||||
let potentialSuggestions = commandsToSearch.filter(
|
||||
(cmd) =>
|
||||
cmd.name.startsWith(partialCommand) ||
|
||||
cmd.altName?.startsWith(partialCommand),
|
||||
)
|
||||
// Filter out ? and any other single character commands unless it's the only char
|
||||
.filter((cmd) => {
|
||||
const nameMatch = cmd.name.startsWith(partialCommand);
|
||||
const altNameMatch = cmd.altName?.startsWith(partialCommand);
|
||||
if (partialCommand.length === 1) {
|
||||
return nameMatch || altNameMatch; // Allow single char match if query is single char
|
||||
}
|
||||
return (
|
||||
(nameMatch && cmd.name.length > 1) ||
|
||||
(altNameMatch && cmd.altName && cmd.altName.length > 1)
|
||||
);
|
||||
})
|
||||
.filter((cmd) => cmd.description)
|
||||
.map((cmd) => ({
|
||||
label: cmd.name, // Always show the main name as label
|
||||
value: cmd.name, // Value should be the main command name for execution
|
||||
description: cmd.description,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
cmd.description &&
|
||||
(cmd.name.startsWith(partial) || cmd.altName?.startsWith(partial)),
|
||||
);
|
||||
|
||||
setSuggestions(filteredSuggestions);
|
||||
setShowSuggestions(filteredSuggestions.length > 0);
|
||||
setActiveSuggestionIndex(filteredSuggestions.length > 0 ? 0 : -1);
|
||||
setVisibleStartIndex(0);
|
||||
setIsLoadingSuggestions(false);
|
||||
// If a user's input is an exact match and it is a leaf command,
|
||||
// enter should submit immediately.
|
||||
if (potentialSuggestions.length > 0 && !hasTrailingSpace) {
|
||||
const perfectMatch = potentialSuggestions.find(
|
||||
(s) => s.name === partial,
|
||||
);
|
||||
if (perfectMatch && !perfectMatch.subCommands) {
|
||||
potentialSuggestions = [];
|
||||
}
|
||||
}
|
||||
|
||||
const finalSuggestions = potentialSuggestions.map((cmd) => ({
|
||||
label: cmd.name,
|
||||
value: cmd.name,
|
||||
description: cmd.description,
|
||||
}));
|
||||
|
||||
setSuggestions(finalSuggestions);
|
||||
setShowSuggestions(finalSuggestions.length > 0);
|
||||
setActiveSuggestionIndex(finalSuggestions.length > 0 ? 0 : -1);
|
||||
setIsLoadingSuggestions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we fall through, no suggestions are available.
|
||||
resetCompletionState();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Handle At Command Completion ---
|
||||
// Handle At Command Completion
|
||||
const atIndex = query.lastIndexOf('@');
|
||||
if (atIndex === -1) {
|
||||
resetCompletionState();
|
||||
@@ -451,7 +506,15 @@ export function useCompletion(
|
||||
isMounted = false;
|
||||
clearTimeout(debounceTimeout);
|
||||
};
|
||||
}, [query, cwd, isActive, resetCompletionState, slashCommands, config]);
|
||||
}, [
|
||||
query,
|
||||
cwd,
|
||||
isActive,
|
||||
resetCompletionState,
|
||||
slashCommands,
|
||||
commandContext,
|
||||
config,
|
||||
]);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
|
||||
Reference in New Issue
Block a user