Added bang(!) commands as a shell passthrough
This commit is contained in:
parent
68a3020044
commit
5f5edb4c9b
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Newline, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { SlashCommand } from '../hooks/slashCommandProcessor.js';
|
import { SlashCommand } from '../hooks/slashCommandProcessor.js';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const Intro: React.FC<Intro> = ({ commands }) => (
|
||||||
* Semantically search and explain code
|
* Semantically search and explain code
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={Colors.Foreground}> * Execute bash commands</Text>
|
<Text color={Colors.Foreground}> * Execute bash commands</Text>
|
||||||
<Newline />
|
<Box height={1} />
|
||||||
<Text bold color={Colors.Foreground}>
|
<Text bold color={Colors.Foreground}>
|
||||||
Commands:
|
Commands:
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -37,5 +37,19 @@ export const Intro: React.FC<Intro> = ({ commands }) => (
|
||||||
{command.description && ' - ' + command.description}
|
{command.description && ' - ' + command.description}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
<Text color={Colors.SubtleComment}>
|
||||||
|
<Text bold color={Colors.AccentPurple}>
|
||||||
|
{' '}
|
||||||
|
!{' '}
|
||||||
|
</Text>
|
||||||
|
shell command
|
||||||
|
</Text>
|
||||||
|
<Text color={Colors.SubtleComment}>
|
||||||
|
<Text bold color={Colors.AccentPurple}>
|
||||||
|
{' '}
|
||||||
|
${' '}
|
||||||
|
</Text>
|
||||||
|
echo hello world
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useCallback } from 'react';
|
||||||
import { Config } from '@gemini-code/server';
|
import { Config } from '@gemini-code/server';
|
||||||
import { type PartListUnion } from '@google/genai';
|
import { type PartListUnion } from '@google/genai';
|
||||||
import { HistoryItem, StreamingState } from '../types.js';
|
import { HistoryItem, StreamingState } from '../types.js';
|
||||||
|
import { getCommandFromQuery } from '../utils/commandUtils.js';
|
||||||
|
|
||||||
// Helper function (consider moving to a shared util if used elsewhere)
|
// Helper function (consider moving to a shared util if used elsewhere)
|
||||||
const addHistoryItem = (
|
const addHistoryItem = (
|
||||||
|
@ -40,15 +41,14 @@ export const usePassthroughProcessor = (
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passthrough commands don't start with special characters like '/' or '@'
|
const [symbol, command] = getCommandFromQuery(trimmedQuery);
|
||||||
if (trimmedQuery.startsWith('/') || trimmedQuery.startsWith('@')) {
|
|
||||||
|
// Passthrough commands don't start with symbol
|
||||||
|
if (symbol !== undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandParts = trimmedQuery.split(/\s+/);
|
if (config.getPassthroughCommands().includes(command)) {
|
||||||
const commandName = commandParts[0];
|
|
||||||
|
|
||||||
if (config.getPassthroughCommands().includes(commandName)) {
|
|
||||||
// Add user message *before* execution starts
|
// Add user message *before* execution starts
|
||||||
const userMessageTimestamp = Date.now();
|
const userMessageTimestamp = Date.now();
|
||||||
addHistoryItem(
|
addHistoryItem(
|
||||||
|
@ -60,7 +60,7 @@ export const usePassthroughProcessor = (
|
||||||
// Execute and capture output
|
// Execute and capture output
|
||||||
const targetDir = config.getTargetDir();
|
const targetDir = config.getTargetDir();
|
||||||
setDebugMessage(
|
setDebugMessage(
|
||||||
`Executing shell command in ${targetDir}: ${trimmedQuery}`,
|
`Executing pass through command in ${targetDir}: ${trimmedQuery}`,
|
||||||
);
|
);
|
||||||
const execOptions = {
|
const execOptions = {
|
||||||
cwd: targetDir,
|
cwd: targetDir,
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exec as _exec } from 'child_process';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Config } from '@gemini-code/server';
|
||||||
|
import { type PartListUnion } from '@google/genai';
|
||||||
|
import { HistoryItem, StreamingState } from '../types.js';
|
||||||
|
import { getCommandFromQuery } from '../utils/commandUtils.js';
|
||||||
|
|
||||||
|
// Helper function (consider moving to a shared util if used elsewhere)
|
||||||
|
const addHistoryItem = (
|
||||||
|
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
||||||
|
itemData: Omit<HistoryItem, 'id'>,
|
||||||
|
id: number,
|
||||||
|
) => {
|
||||||
|
setHistory((prevHistory) => [
|
||||||
|
...prevHistory,
|
||||||
|
{ ...itemData, id } as HistoryItem,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShellCommandProcessor = (
|
||||||
|
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
||||||
|
setStreamingState: React.Dispatch<React.SetStateAction<StreamingState>>,
|
||||||
|
setDebugMessage: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
getNextMessageId: (baseTimestamp: number) => number,
|
||||||
|
config: Config,
|
||||||
|
) => {
|
||||||
|
const handleShellCommand = useCallback(
|
||||||
|
(rawQuery: PartListUnion): boolean => {
|
||||||
|
if (typeof rawQuery !== 'string') {
|
||||||
|
return false; // Passthrough only works with string commands
|
||||||
|
}
|
||||||
|
|
||||||
|
const [symbol] = getCommandFromQuery(rawQuery);
|
||||||
|
if (symbol !== '!' && symbol !== '$') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Remove symbol from rawQuery
|
||||||
|
const trimmed = rawQuery.trim().slice(1);
|
||||||
|
|
||||||
|
// Add user message *before* execution starts
|
||||||
|
const userMessageTimestamp = Date.now();
|
||||||
|
addHistoryItem(
|
||||||
|
setHistory,
|
||||||
|
{ type: 'user', text: rawQuery },
|
||||||
|
userMessageTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute and capture output
|
||||||
|
const targetDir = config.getTargetDir();
|
||||||
|
setDebugMessage(`Executing shell command in ${targetDir}: ${trimmed}`);
|
||||||
|
const execOptions = {
|
||||||
|
cwd: targetDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set state to Responding while the command runs
|
||||||
|
setStreamingState(StreamingState.Responding);
|
||||||
|
|
||||||
|
_exec(trimmed, execOptions, (error, stdout, stderr) => {
|
||||||
|
const timestamp = getNextMessageId(userMessageTimestamp); // Use user message time as base
|
||||||
|
if (error) {
|
||||||
|
addHistoryItem(
|
||||||
|
setHistory,
|
||||||
|
{ type: 'error', text: error.message },
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
} else if (stderr) {
|
||||||
|
// Treat stderr as info for passthrough, as some tools use it for non-error output
|
||||||
|
addHistoryItem(setHistory, { type: 'info', text: stderr }, timestamp);
|
||||||
|
} else {
|
||||||
|
// Add stdout as an info message
|
||||||
|
addHistoryItem(
|
||||||
|
setHistory,
|
||||||
|
{ type: 'info', text: stdout || '(Command produced no output)' },
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Set state back to Idle *after* command finishes and output is added
|
||||||
|
setStreamingState(StreamingState.Idle);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true; // Command was handled
|
||||||
|
},
|
||||||
|
[config, setDebugMessage, setHistory, setStreamingState, getNextMessageId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { handleShellCommand };
|
||||||
|
};
|
|
@ -7,7 +7,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { type PartListUnion } from '@google/genai';
|
import { type PartListUnion } from '@google/genai';
|
||||||
import { HistoryItem } from '../types.js';
|
import { HistoryItem } from '../types.js';
|
||||||
import { isSlashCommand } from '../utils/commandUtils.js';
|
import { getCommandFromQuery } from '../utils/commandUtils.js';
|
||||||
|
|
||||||
export interface SlashCommand {
|
export interface SlashCommand {
|
||||||
name: string; // slash command
|
name: string; // slash command
|
||||||
|
@ -88,30 +88,31 @@ export const useSlashCommandProcessor = (
|
||||||
// Removed /theme command, handled in App.tsx
|
// Removed /theme command, handled in App.tsx
|
||||||
];
|
];
|
||||||
|
|
||||||
// Checks if the query is a slash command and executes it if it is.
|
// Checks if the query is a slash command and executes the command if it is.
|
||||||
const handleSlashCommand = useCallback(
|
const handleSlashCommand = useCallback(
|
||||||
(rawQuery: PartListUnion): boolean => {
|
(rawQuery: PartListUnion): boolean => {
|
||||||
if (typeof rawQuery !== 'string') {
|
if (typeof rawQuery !== 'string') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedQuery = rawQuery.trim();
|
const trimmed = rawQuery.trim();
|
||||||
if (!isSlashCommand(trimmedQuery)) {
|
const [symbol, test] = getCommandFromQuery(trimmed);
|
||||||
return false; // Not a slash command
|
|
||||||
|
// Skip non slash commands
|
||||||
|
if (symbol !== '/') {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandName = trimmedQuery.slice(1).split(/\s+/)[0]; // Get command name after '/'
|
|
||||||
|
|
||||||
for (const cmd of slashCommands) {
|
for (const cmd of slashCommands) {
|
||||||
if (commandName === cmd.name) {
|
if (test === cmd.name) {
|
||||||
// Add user message *before* execution
|
// Add user message *before* execution
|
||||||
const userMessageTimestamp = Date.now();
|
const userMessageTimestamp = Date.now();
|
||||||
addHistoryItem(
|
addHistoryItem(
|
||||||
setHistory,
|
setHistory,
|
||||||
{ type: 'user', text: trimmedQuery },
|
{ type: 'user', text: trimmed },
|
||||||
userMessageTimestamp,
|
userMessageTimestamp,
|
||||||
);
|
);
|
||||||
cmd.action(trimmedQuery);
|
cmd.action(trimmed);
|
||||||
return true; // Command was handled
|
return true; // Command was handled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { isAtCommand } from '../utils/commandUtils.js'; // Import the @ command checker
|
import { isAtCommand } from '../utils/commandUtils.js'; // Import the @ command checker
|
||||||
import { useSlashCommandProcessor } from './slashCommandProcessor.js';
|
import { useSlashCommandProcessor } from './slashCommandProcessor.js';
|
||||||
|
import { useShellCommandProcessor } from './shellCommandProcessor.js';
|
||||||
import { usePassthroughProcessor } from './passthroughCommandProcessor.js';
|
import { usePassthroughProcessor } from './passthroughCommandProcessor.js';
|
||||||
import { handleAtCommand } from './atCommandProcessor.js'; // Import the @ command handler
|
import { handleAtCommand } from './atCommandProcessor.js'; // Import the @ command handler
|
||||||
import { findSafeSplitPoint } from '../utils/markdownUtilities.js'; // Import the split point finder
|
import { findSafeSplitPoint } from '../utils/markdownUtilities.js'; // Import the split point finder
|
||||||
|
@ -75,6 +76,14 @@ export const useGeminiStream = (
|
||||||
getNextMessageId,
|
getNextMessageId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { handleShellCommand } = useShellCommandProcessor(
|
||||||
|
setHistory,
|
||||||
|
setStreamingState,
|
||||||
|
setDebugMessage,
|
||||||
|
getNextMessageId,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
const { handlePassthroughCommand } = usePassthroughProcessor(
|
const { handlePassthroughCommand } = usePassthroughProcessor(
|
||||||
setHistory,
|
setHistory,
|
||||||
setStreamingState,
|
setStreamingState,
|
||||||
|
@ -154,14 +163,19 @@ export const useGeminiStream = (
|
||||||
const trimmedQuery = query.trim();
|
const trimmedQuery = query.trim();
|
||||||
setDebugMessage(`User query: '${trimmedQuery}'`);
|
setDebugMessage(`User query: '${trimmedQuery}'`);
|
||||||
|
|
||||||
// 1. Check for Slash Commands
|
// 1. Check for Slash Commands (/)
|
||||||
if (handleSlashCommand(trimmedQuery)) {
|
if (handleSlashCommand(trimmedQuery)) {
|
||||||
return; // Handled, exit
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check for Passthrough Commands
|
// 2. Check for Shell Commands (! or $)
|
||||||
|
if (handleShellCommand(trimmedQuery)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check for Passthrough Commands
|
||||||
if (handlePassthroughCommand(trimmedQuery)) {
|
if (handlePassthroughCommand(trimmedQuery)) {
|
||||||
return; // Handled, exit
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check for @ Commands using the utility function
|
// 3. Check for @ Commands using the utility function
|
||||||
|
|
|
@ -16,11 +16,19 @@ export const isAtCommand = (query: string): boolean =>
|
||||||
// Check if starts with @ OR has a space, then @, then a non-space character.
|
// Check if starts with @ OR has a space, then @, then a non-space character.
|
||||||
query.startsWith('@') || /\s@\S/.test(query);
|
query.startsWith('@') || /\s@\S/.test(query);
|
||||||
|
|
||||||
|
const control_symbols: string[] = ['/', '@', '!', '?', '$'];
|
||||||
/**
|
/**
|
||||||
* Checks if a query string represents a slash command (starts with '/').
|
* Returns the first word of query with optional leading slash, ampersand, bang.
|
||||||
*
|
*
|
||||||
* @param query The input query string.
|
* @param query The input query string.
|
||||||
* @returns True if the query is a slash command, false otherwise.
|
* @returns optional leading symbol and first word of query
|
||||||
*/
|
*/
|
||||||
export const isSlashCommand = (query: string): boolean =>
|
export const getCommandFromQuery = (
|
||||||
query.trim().startsWith('/');
|
query: string,
|
||||||
|
): [string | undefined, string] => {
|
||||||
|
const word = query.trim().split(/\s/, 1)[0];
|
||||||
|
if (word.length > 0 && control_symbols.includes(word[0])) {
|
||||||
|
return [word[0], word.slice(1)];
|
||||||
|
}
|
||||||
|
return [undefined, word];
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue