summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx5
-rw-r--r--packages/cli/src/ui/components/SuggestionsDisplay.tsx9
-rw-r--r--packages/cli/src/ui/hooks/atCommandProcessor.ts98
-rw-r--r--packages/cli/src/ui/hooks/useCompletion.ts24
4 files changed, 113 insertions, 23 deletions
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 67727fd2..fbf84766 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -8,6 +8,7 @@ import React, { useCallback } from 'react';
import { Text, Box, useInput, useFocus, Key } from 'ink';
import TextInput from 'ink-text-input';
import { Colors } from '../colors.js';
+import { Suggestion } from './SuggestionsDisplay.js';
interface InputPromptProps {
query: string;
@@ -16,7 +17,7 @@ interface InputPromptProps {
setInputKey: React.Dispatch<React.SetStateAction<number>>;
onSubmit: (value: string) => void;
showSuggestions: boolean;
- suggestions: string[];
+ suggestions: Suggestion[]; // Changed to Suggestion[]
activeSuggestionIndex: number;
navigateUp: () => void;
navigateDown: () => void;
@@ -63,7 +64,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1);
}
- const newValue = base + selectedSuggestion;
+ const newValue = base + selectedSuggestion.value;
setQuery(newValue);
resetCompletion(); // Hide suggestions after selection
setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx
index a9d24003..8c9cf377 100644
--- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx
+++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx
@@ -6,9 +6,12 @@
import React from 'react';
import { Box, Text } from 'ink';
-
+export interface Suggestion {
+ label: string;
+ value: string;
+}
interface SuggestionsDisplayProps {
- suggestions: string[];
+ suggestions: Suggestion[];
activeIndex: number;
isLoading: boolean;
width: number;
@@ -62,7 +65,7 @@ export function SuggestionsDisplay({
color={isActive ? 'black' : 'white'}
backgroundColor={isActive ? 'blue' : undefined}
>
- {suggestion}
+ {suggestion.label}
</Text>
);
})}
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts
index da3d71dd..09adc7c0 100644
--- a/packages/cli/src/ui/hooks/atCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts
@@ -7,7 +7,12 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { PartListUnion } from '@google/genai';
-import { Config, getErrorMessage, isNodeError } from '@gemini-code/server';
+import {
+ Config,
+ getErrorMessage,
+ isNodeError,
+ unescapePath,
+} from '@gemini-code/server';
import {
HistoryItem,
IndividualToolCallDisplay,
@@ -40,6 +45,54 @@ interface HandleAtCommandResult {
}
/**
+ * Parses a query string to find the first '@<path>' command,
+ * handling \ escaped spaces within the path.
+ */
+function parseAtCommand(
+ query: string,
+): { textBefore: string; atPath: string; textAfter: string } | null {
+ let atIndex = -1;
+ for (let i = 0; i < query.length; i++) {
+ // Find the first '@' that is not preceded by a '\'
+ if (query[i] === '@' && (i === 0 || query[i - 1] !== '\\')) {
+ atIndex = i;
+ break;
+ }
+ }
+
+ if (atIndex === -1) {
+ return null; // No '@' command found
+ }
+
+ const textBefore = query.substring(0, atIndex).trim();
+ let pathEndIndex = atIndex + 1;
+ let inEscape = false;
+
+ while (pathEndIndex < query.length) {
+ const char = query[pathEndIndex];
+
+ if (inEscape) {
+ // Current char is escaped, move past it
+ inEscape = false;
+ } else if (char === '\\') {
+ // Start of an escape sequence
+ inEscape = true;
+ } else if (/\s/.test(char)) {
+ // Unescaped whitespace marks the end of the path
+ break;
+ }
+ pathEndIndex++;
+ }
+
+ const rawAtPath = query.substring(atIndex, pathEndIndex);
+ const textAfter = query.substring(pathEndIndex).trim();
+
+ const atPath = unescapePath(rawAtPath);
+
+ return { textBefore, atPath, textAfter };
+}
+
+/**
* Processes user input potentially containing an '@<path>' command.
* It finds the first '@<path>', checks if the path is a file or directory,
* prepares the appropriate path specification for the read_many_files tool,
@@ -58,25 +111,50 @@ export async function handleAtCommand({
userMessageTimestamp,
}: HandleAtCommandParams): Promise<HandleAtCommandResult> {
const trimmedQuery = query.trim();
+ const parsedCommand = parseAtCommand(trimmedQuery);
- const atCommandRegex = /^(.*?)(@\S+)(.*)$/s;
- const match = trimmedQuery.match(atCommandRegex);
-
- if (!match) {
+ if (!parsedCommand) {
+ // If no '@' was found, treat the whole query as user text and proceed
+ // This allows users to just type text without an @ command
+ addHistoryItem(
+ setHistory,
+ { type: 'user', text: query },
+ userMessageTimestamp,
+ );
+ // Let the main hook decide what to do (likely send to LLM)
+ return { processedQuery: [{ text: query }], shouldProceed: true };
+ // Or, if an @ command is *required* when the function is called:
+ /*
const errorTimestamp = getNextMessageId(userMessageTimestamp);
addHistoryItem(
setHistory,
- { type: 'error', text: 'Error: Could not parse @ command.' },
+ { type: 'error', text: 'Error: Could not find @ command.' },
errorTimestamp,
);
return { processedQuery: null, shouldProceed: false };
+ */
}
- const textBefore = match[1].trim();
- const atPath = match[2];
- const textAfter = match[3].trim();
+ const { textBefore, atPath, textAfter } = parsedCommand;
+
+ // Add the original user query to history *before* processing
+ addHistoryItem(
+ setHistory,
+ { type: 'user', text: query },
+ userMessageTimestamp,
+ );
+
+ const pathPart = atPath.substring(1); // Remove the leading '@'
- const pathPart = atPath.substring(1);
+ if (!pathPart) {
+ const errorTimestamp = getNextMessageId(userMessageTimestamp);
+ addHistoryItem(
+ setHistory,
+ { type: 'error', text: 'Error: No path specified after @.' },
+ errorTimestamp,
+ );
+ return { processedQuery: null, shouldProceed: false };
+ }
addHistoryItem(
setHistory,
diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts
index 36aed0d1..07a71630 100644
--- a/packages/cli/src/ui/hooks/useCompletion.ts
+++ b/packages/cli/src/ui/hooks/useCompletion.ts
@@ -7,11 +7,13 @@
import { useState, useEffect, useCallback } from 'react';
import * as fs from 'fs/promises';
import * as path from 'path';
-import { isNodeError } from '@gemini-code/server';
-import { MAX_SUGGESTIONS_TO_SHOW } from '../components/SuggestionsDisplay.js';
-
+import { isNodeError, escapePath, unescapePath } from '@gemini-code/server';
+import {
+ MAX_SUGGESTIONS_TO_SHOW,
+ Suggestion,
+} from '../components/SuggestionsDisplay.js';
export interface UseCompletionReturn {
- suggestions: string[];
+ suggestions: Suggestion[];
activeSuggestionIndex: number;
visibleStartIndex: number;
showSuggestions: boolean;
@@ -28,7 +30,7 @@ export function useCompletion(
cwd: string,
isActive: boolean,
): UseCompletionReturn {
- const [suggestions, setSuggestions] = useState<string[]>([]);
+ const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [activeSuggestionIndex, setActiveSuggestionIndex] =
useState<number>(-1);
const [visibleStartIndex, setVisibleStartIndex] = useState<number>(0);
@@ -121,10 +123,12 @@ export function useCompletion(
lastSlashIndex === -1
? '.'
: partialPath.substring(0, lastSlashIndex + 1);
- const prefix =
+ const prefix = unescapePath(
lastSlashIndex === -1
? partialPath
- : partialPath.substring(lastSlashIndex + 1);
+ : partialPath.substring(lastSlashIndex + 1),
+ );
+
const baseDirAbsolute = path.resolve(cwd, baseDirRelative);
let isMounted = true;
@@ -144,7 +148,11 @@ export function useCompletion(
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return a.localeCompare(b);
- });
+ })
+ .map((entry) => ({
+ label: entry,
+ value: escapePath(entry),
+ }));
if (isMounted) {
setSuggestions(filteredSuggestions);