Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4ed2921
feat(shell): enable interactive commands with virtual terminal
galz10 Aug 20, 2025
324fe6b
Centralize terminal size management in Config
galz10 Aug 22, 2025
5fec66e
Merge remote-tracking branch 'origin/main' into galzahavi/add/vterminal
galz10 Aug 22, 2025
d7fb48b
refactor: move active shell pty management to useShellCommandProcessor
galz10 Aug 22, 2025
ac8ff7d
Refactor: Co-locate shell focus logic and remove useEffect
galz10 Aug 23, 2025
16c2698
feat(terminal): Implement true cursor positioning in shell UI
galz10 Aug 26, 2025
c0bb435
refactor(core): Optimize terminal rendering latency
galz10 Aug 27, 2025
961ec28
Make shell cursor non-blinking
galz10 Aug 27, 2025
8698742
Merge remote-tracking branch 'origin/main' into galzahavi/add/vterminal
galz10 Aug 27, 2025
4548d0c
Merge branch 'main' into galzahavi/add/vterminal
galz10 Aug 27, 2025
3c540a7
Fix test failure
galz10 Aug 27, 2025
203f9b4
Cleanup Code
galz10 Aug 27, 2025
b2a3586
Remove unused config prop
galz10 Aug 27, 2025
52b963a
Introduce shellExecutionConfig for tool execution
galz10 Aug 30, 2025
29dcf0e
Introduce shellExecutionConfig for tool execution
galz10 Aug 30, 2025
85e8062
Add ANSI color and style support
galz10 Sep 3, 2025
1504c99
Merge branch 'galzahavi/add/vterminal' of github.com:google-gemini/ge…
galz10 Sep 3, 2025
77e0437
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 4, 2025
cbe2e2c
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 4, 2025
dfa3805
feat(shell): Add settings for pager and color output
galz10 Sep 4, 2025
a9f3e5f
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 4, 2025
29be569
On exit output terminal buffer as string
galz10 Sep 4, 2025
255faa9
Render shell output using structured ANSI data
galz10 Sep 5, 2025
90197ce
Fix failing unit tests
galz10 Sep 5, 2025
5253ef3
Add unit tests
galz10 Sep 5, 2025
808c361
enhance ShellTool with live PTY output
galz10 Sep 7, 2025
8c2b849
Address code review concerns
galz10 Sep 7, 2025
9fe8d64
Added more unit tests
galz10 Sep 8, 2025
65e8c38
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 9, 2025
4a18866
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 9, 2025
6fb34b5
Address PR comments
galz10 Sep 11, 2025
707d511
Merge branch 'galzahavi/add/vterminal' of github.com:google-gemini/ge…
galz10 Sep 11, 2025
5e710d4
Rename isShellInputFocused to isShellFocused
galz10 Sep 11, 2025
454ccb1
Address PR comments
galz10 Sep 11, 2025
22a524b
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 11, 2025
328d7ce
Remove reporting response when process hasn't completed for child_pro…
galz10 Sep 11, 2025
8e8fa89
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 11, 2025
cd3534f
Merge branch 'main' into galzahavi/add/vterminal
galz10 Sep 11, 2025
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
16 changes: 13 additions & 3 deletions packages/a2a-server/src/agent/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
ToolCallConfirmationDetails,
Config,
UserTierId,
AnsiOutput,
} from '@google/gemini-cli-core';
import type { RequestContext } from '@a2a-js/sdk/server';
import { type ExecutionEventBus } from '@a2a-js/sdk/server';
Expand Down Expand Up @@ -284,20 +285,29 @@ export class Task {

private _schedulerOutputUpdate(
toolCallId: string,
outputChunk: string,
outputChunk: string | AnsiOutput,
): void {
let outputAsText: string;
if (typeof outputChunk === 'string') {
outputAsText = outputChunk;
} else {
outputAsText = outputChunk
.map((line) => line.map((token) => token.text).join(''))
.join('\n');
}

logger.info(
'[Task] Scheduler output update for tool call ' +
toolCallId +
': ' +
outputChunk,
outputAsText,
);
const artifact: Artifact = {
artifactId: `tool-${toolCallId}-output`,
parts: [
{
kind: 'text',
text: outputChunk,
text: outputAsText,
} as Part,
],
};
Expand Down
2 changes: 2 additions & 0 deletions packages/a2a-server/src/http/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ vi.mock('../utils/logger.js', () => ({
let config: Config;
const getToolRegistrySpy = vi.fn().mockReturnValue(ApprovalMode.DEFAULT);
const getApprovalModeSpy = vi.fn();
const getShellExecutionConfigSpy = vi.fn();
vi.mock('../config/config.js', async () => {
const actual = await vi.importActual('../config/config.js');
return {
Expand All @@ -72,6 +73,7 @@ vi.mock('../config/config.js', async () => {
const mockConfig = createMockConfig({
getToolRegistry: getToolRegistrySpy,
getApprovalMode: getApprovalModeSpy,
getShellExecutionConfig: getShellExecutionConfigSpy,
});
config = mockConfig as Config;
return config;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export enum Command {
REVERSE_SEARCH = 'reverseSearch',
SUBMIT_REVERSE_SEARCH = 'submitReverseSearch',
ACCEPT_SUGGESTION_REVERSE_SEARCH = 'acceptSuggestionReverseSearch',
TOGGLE_SHELL_INPUT_FOCUS = 'toggleShellInputFocus',
}

/**
Expand Down Expand Up @@ -162,4 +163,5 @@ export const defaultKeyBindings: KeyBindingConfig = {
// Note: original logic ONLY checked ctrl=false, ignored meta/shift/paste
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
[Command.TOGGLE_SHELL_INPUT_FOCUS]: [{ key: 'f', ctrl: true }],
};
2 changes: 2 additions & 0 deletions packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ const MIGRATION_MAP: Record<string, string> = {
sandbox: 'tools.sandbox',
selectedAuthType: 'security.auth.selectedType',
shouldUseNodePtyShell: 'tools.usePty',
shellPager: 'tools.shell.pager',
shellShowColor: 'tools.shell.showColor',
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
summarizeToolOutput: 'model.summarizeToolOutput',
telemetry: 'telemetry',
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,36 @@ const SETTINGS_SCHEMA = {
'Use node-pty for shell command execution. Fallback to child_process still applies.',
showInDialog: true,
},
shell: {
type: 'object',
label: 'Shell',
category: 'Tools',
requiresRestart: false,
default: {},
description: 'Settings for shell execution.',
showInDialog: false,
properties: {
pager: {
type: 'string',
label: 'Pager',
category: 'Tools',
requiresRestart: false,
default: 'cat' as string | undefined,
description:
'The pager command to use for shell output. Defaults to `cat`.',
showInDialog: false,
},
showColor: {
type: 'boolean',
label: 'Show Color',
category: 'Tools',
requiresRestart: false,
default: false,
description: 'Show color in shell output.',
showInDialog: true,
},
},
},
autoAccept: {
type: 'boolean',
label: 'Auto Accept',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe('ShellProcessor', () => {
getTargetDir: vi.fn().mockReturnValue('/test/dir'),
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getShouldUseNodePtyShell: vi.fn().mockReturnValue(false),
getShellExecutionConfig: vi.fn().mockReturnValue({}),
};

context = createMockCommandContext({
Expand Down Expand Up @@ -147,6 +148,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
expect(result).toEqual([{ text: 'The current status is: On branch main' }]);
});
Expand Down Expand Up @@ -218,6 +220,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]);
});
Expand Down Expand Up @@ -410,6 +413,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
});

Expand Down Expand Up @@ -574,6 +578,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);

expect(result).toEqual([{ text: 'Command: match found' }]);
Expand All @@ -598,6 +603,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);

expect(result).toEqual([
Expand Down Expand Up @@ -668,6 +674,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
});

Expand Down Expand Up @@ -697,6 +704,7 @@ describe('ShellProcessor', () => {
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
SHORTHAND_ARGS_PLACEHOLDER,
} from './types.js';
import { extractInjections, type Injection } from './injectionParser.js';
import { themeManager } from '../../ui/themes/theme-manager.js';

export class ConfirmationRequiredError extends Error {
constructor(
Expand Down Expand Up @@ -159,12 +160,19 @@ export class ShellProcessor implements IPromptProcessor {

// Execute the resolved command (which already has ESCAPED input).
if (injection.resolvedCommand) {
const activeTheme = themeManager.getActiveTheme();
const shellExecutionConfig = {
...config.getShellExecutionConfig(),
defaultFg: activeTheme.colors.Foreground,
defaultBg: activeTheme.colors.Background,
};
const { result } = await ShellExecutionService.execute(
injection.resolvedCommand,
config.getTargetDir(),
() => {},
new AbortController().signal,
config.getShouldUseNodePtyShell(),
shellExecutionConfig,
);

const executionResult = await result;
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
getAllGeminiMdFilenames,
AuthType,
clearCachedCredentialFile,
ShellExecutionService,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
Expand Down Expand Up @@ -97,6 +98,18 @@ interface AppContainerProps {
initializationResult: InitializationResult;
}

/**
* The fraction of the terminal width to allocate to the shell.
* This provides horizontal padding.
*/
const SHELL_WIDTH_FRACTION = 0.89;

/**
* The number of lines to subtract from the available terminal height
* for the shell. This provides vertical padding and space for other UI elements.
*/
const SHELL_HEIGHT_PADDING = 10;

export const AppContainer = (props: AppContainerProps) => {
const { settings, config, initializationResult } = props;
const historyManager = useHistory();
Expand All @@ -110,6 +123,8 @@ export const AppContainer = (props: AppContainerProps) => {
initializationResult.themeError,
);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [shellFocused, setShellFocused] = useState(false);

const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(
initializationResult.geminiMdFileCount,
);
Expand Down Expand Up @@ -506,6 +521,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
pendingHistoryItems: pendingGeminiHistoryItems,
thought,
cancelOngoingRequest,
activePtyId,
loopDetectionConfirmationRequest,
} = useGeminiStream(
config.getGeminiClient(),
Expand All @@ -523,6 +539,10 @@ Logging in with Google... Please restart Gemini CLI to continue.
setModelSwitchedFromQuotaError,
refreshStatic,
() => cancelHandlerRef.current(),
setShellFocused,
terminalWidth,
terminalHeight,
shellFocused,
);

const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
Expand Down Expand Up @@ -603,6 +623,13 @@ Logging in with Google... Please restart Gemini CLI to continue.
return terminalHeight - staticExtraHeight;
}, [terminalHeight]);

config.setShellExecutionConfig({
terminalWidth: Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
terminalHeight: Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING),
pager: settings.merged.tools?.shell?.pager,
showColor: settings.merged.tools?.shell?.showColor,
});

const isFocused = useFocus();
useBracketedPaste();

Expand All @@ -620,6 +647,22 @@ Logging in with Google... Please restart Gemini CLI to continue.
const initialPromptSubmitted = useRef(false);
const geminiClient = config.getGeminiClient();

useEffect(() => {
if (activePtyId) {
ShellExecutionService.resizePty(
activePtyId,
Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING),
);
}
}, [
terminalHeight,
terminalWidth,
availableTerminalHeight,
activePtyId,
geminiClient,
]);

useEffect(() => {
if (
initialPrompt &&
Expand Down Expand Up @@ -840,6 +883,10 @@ Logging in with Google... Please restart Gemini CLI to continue.
!enteringConstrainHeightMode
) {
setConstrainHeight(false);
} else if (keyMatchers[Command.TOGGLE_SHELL_INPUT_FOCUS](key)) {
if (activePtyId || shellFocused) {
setShellFocused((prev) => !prev);
}
}
},
[
Expand All @@ -866,6 +913,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
isSettingsDialogOpen,
isFolderTrustDialogOpen,
showPrivacyNotice,
activePtyId,
shellFocused,
settings.merged.general?.debugKeystrokeLogging,
],
);
Expand Down Expand Up @@ -991,6 +1040,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
updateInfo,
showIdeRestartPrompt,
isRestarting,
activePtyId,
shellFocused,
}),
[
historyManager.history,
Expand Down Expand Up @@ -1064,6 +1115,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
showIdeRestartPrompt,
isRestarting,
currentModel,
activePtyId,
shellFocused,
],
);

Expand Down
Loading
Loading