Added bang(!) commands as a shell passthrough

This commit is contained in:
Seth Troisi
2025-04-30 00:26:07 +00:00
parent 68a3020044
commit 5f5edb4c9b
6 changed files with 157 additions and 27 deletions

View File

@@ -9,6 +9,7 @@ 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 = (
@@ -40,15 +41,14 @@ export const usePassthroughProcessor = (
return false;
}
// Passthrough commands don't start with special characters like '/' or '@'
if (trimmedQuery.startsWith('/') || trimmedQuery.startsWith('@')) {
const [symbol, command] = getCommandFromQuery(trimmedQuery);
// Passthrough commands don't start with symbol
if (symbol !== undefined) {
return false;
}
const commandParts = trimmedQuery.split(/\s+/);
const commandName = commandParts[0];
if (config.getPassthroughCommands().includes(commandName)) {
if (config.getPassthroughCommands().includes(command)) {
// Add user message *before* execution starts
const userMessageTimestamp = Date.now();
addHistoryItem(
@@ -60,7 +60,7 @@ export const usePassthroughProcessor = (
// Execute and capture output
const targetDir = config.getTargetDir();
setDebugMessage(
`Executing shell command in ${targetDir}: ${trimmedQuery}`,
`Executing pass through command in ${targetDir}: ${trimmedQuery}`,
);
const execOptions = {
cwd: targetDir,

View File

@@ -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 };
};

View File

@@ -7,7 +7,7 @@
import { useCallback } from 'react';
import { type PartListUnion } from '@google/genai';
import { HistoryItem } from '../types.js';
import { isSlashCommand } from '../utils/commandUtils.js';
import { getCommandFromQuery } from '../utils/commandUtils.js';
export interface SlashCommand {
name: string; // slash command
@@ -88,30 +88,31 @@ export const useSlashCommandProcessor = (
// 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(
(rawQuery: PartListUnion): boolean => {
if (typeof rawQuery !== 'string') {
return false;
}
const trimmedQuery = rawQuery.trim();
if (!isSlashCommand(trimmedQuery)) {
return false; // Not a slash command
const trimmed = rawQuery.trim();
const [symbol, test] = getCommandFromQuery(trimmed);
// 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) {
if (commandName === cmd.name) {
if (test === cmd.name) {
// Add user message *before* execution
const userMessageTimestamp = Date.now();
addHistoryItem(
setHistory,
{ type: 'user', text: trimmedQuery },
{ type: 'user', text: trimmed },
userMessageTimestamp,
);
cmd.action(trimmedQuery);
cmd.action(trimmed);
return true; // Command was handled
}
}

View File

@@ -29,6 +29,7 @@ import {
} from '../types.js';
import { isAtCommand } from '../utils/commandUtils.js'; // Import the @ command checker
import { useSlashCommandProcessor } from './slashCommandProcessor.js';
import { useShellCommandProcessor } from './shellCommandProcessor.js';
import { usePassthroughProcessor } from './passthroughCommandProcessor.js';
import { handleAtCommand } from './atCommandProcessor.js'; // Import the @ command handler
import { findSafeSplitPoint } from '../utils/markdownUtilities.js'; // Import the split point finder
@@ -75,6 +76,14 @@ export const useGeminiStream = (
getNextMessageId,
);
const { handleShellCommand } = useShellCommandProcessor(
setHistory,
setStreamingState,
setDebugMessage,
getNextMessageId,
config,
);
const { handlePassthroughCommand } = usePassthroughProcessor(
setHistory,
setStreamingState,
@@ -154,14 +163,19 @@ export const useGeminiStream = (
const trimmedQuery = query.trim();
setDebugMessage(`User query: '${trimmedQuery}'`);
// 1. Check for Slash Commands
// 1. Check for Slash Commands (/)
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)) {
return; // Handled, exit
return;
}
// 3. Check for @ Commands using the utility function