mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-23 09:47:45 -07:00
feat: Implement background shell commands (#14849)
This commit is contained in:
@@ -99,6 +99,7 @@ import { computeTerminalTitle } from '../utils/windowTitle.js';
|
||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||
import { useLogger } from './hooks/useLogger.js';
|
||||
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||
import { type BackgroundShell } from './hooks/shellCommandProcessor.js';
|
||||
import { useVim } from './hooks/vim.js';
|
||||
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
|
||||
import { type InitializationResult } from '../core/initializer.js';
|
||||
@@ -138,6 +139,7 @@ import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'
|
||||
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||
import { useBanner } from './hooks/useBanner.js';
|
||||
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
|
||||
import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js';
|
||||
import {
|
||||
WARNING_PROMPT_DURATION_MS,
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
@@ -259,6 +261,10 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
|
||||
const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false);
|
||||
const toggleBackgroundShellRef = useRef<() => void>(() => {});
|
||||
const isBackgroundShellVisibleRef = useRef<boolean>(false);
|
||||
const backgroundShellsRef = useRef<Map<number, BackgroundShell>>(new Map());
|
||||
|
||||
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
|
||||
|
||||
const [shellModeActive, setShellModeActive] = useState(false);
|
||||
@@ -489,6 +495,12 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
registerCleanup(async () => {
|
||||
// Turn off mouse scroll.
|
||||
disableMouseEvents();
|
||||
|
||||
// Kill all background shells
|
||||
for (const pid of backgroundShellsRef.current.keys()) {
|
||||
ShellExecutionService.kill(pid);
|
||||
}
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.disconnect();
|
||||
|
||||
@@ -837,6 +849,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const { toggleVimEnabled } = useVimMode();
|
||||
|
||||
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
|
||||
() => {},
|
||||
);
|
||||
|
||||
const slashCommandActions = useMemo(
|
||||
() => ({
|
||||
openAuthDialog: () => setAuthState(AuthState.Updating),
|
||||
@@ -860,6 +876,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
toggleDebugProfiler,
|
||||
dispatchExtensionStateUpdate,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
toggleBackgroundShell: () => {
|
||||
toggleBackgroundShellRef.current();
|
||||
if (!isBackgroundShellVisibleRef.current) {
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundShellsRef.current.size > 1) {
|
||||
setIsBackgroundShellListOpenRef.current(true);
|
||||
} else {
|
||||
setIsBackgroundShellListOpenRef.current(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
setText: (text: string) => buffer.setText(text),
|
||||
}),
|
||||
[
|
||||
@@ -1011,6 +1038,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
@@ -1033,7 +1066,30 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
embeddedShellFocused,
|
||||
);
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
isBackgroundShellVisibleRef.current = isBackgroundShellVisible;
|
||||
backgroundShellsRef.current = backgroundShells;
|
||||
|
||||
const {
|
||||
activeBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
isBackgroundShellListOpen,
|
||||
setActiveBackgroundShellPid,
|
||||
backgroundShellHeight,
|
||||
} = useBackgroundShellManager({
|
||||
backgroundShells,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
setEmbeddedShellFocused,
|
||||
terminalHeight,
|
||||
});
|
||||
|
||||
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
|
||||
|
||||
const lastOutputTimeRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
lastOutputTimeRef.current = lastOutputTime;
|
||||
}, [lastOutputTime]);
|
||||
@@ -1182,7 +1238,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
// Compute available terminal height based on controls measurement
|
||||
const availableTerminalHeight = Math.max(
|
||||
0,
|
||||
terminalHeight - controlsHeight - staticExtraHeight - 2,
|
||||
terminalHeight -
|
||||
controlsHeight -
|
||||
staticExtraHeight -
|
||||
2 -
|
||||
backgroundShellHeight,
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
@@ -1542,9 +1602,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setConstrainHeight(false);
|
||||
return true;
|
||||
} else if (
|
||||
keyMatchers[Command.UNFOCUS_SHELL_INPUT](key) &&
|
||||
activePtyId &&
|
||||
embeddedShellFocused
|
||||
keyMatchers[Command.FOCUS_SHELL_INPUT](key) &&
|
||||
(activePtyId ||
|
||||
(isBackgroundShellVisible && backgroundShells.size > 0)) &&
|
||||
buffer.text.length === 0
|
||||
) {
|
||||
if (key.name === 'tab' && key.shift) {
|
||||
// Always change focus
|
||||
@@ -1552,26 +1613,72 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (embeddedShellFocused) {
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
// If the shell hasn't produced output in the last 100ms, it's considered idle.
|
||||
const isIdle = now - lastOutputTimeRef.current >= 100;
|
||||
if (isIdle) {
|
||||
if (isIdle && !activePtyId) {
|
||||
if (tabFocusTimeoutRef.current) {
|
||||
clearTimeout(tabFocusTimeoutRef.current);
|
||||
}
|
||||
tabFocusTimeoutRef.current = setTimeout(() => {
|
||||
tabFocusTimeoutRef.current = null;
|
||||
// If the shell produced output since the tab press, we assume it handled the tab
|
||||
// (e.g. autocomplete) so we should not toggle focus.
|
||||
if (lastOutputTimeRef.current > now) {
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
return;
|
||||
toggleBackgroundShell();
|
||||
if (!isBackgroundShellVisible) {
|
||||
// We are about to show it, so focus it
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundShells.size > 1) {
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
setEmbeddedShellFocused(false);
|
||||
}, 100);
|
||||
} else {
|
||||
// We are about to hide it
|
||||
tabFocusTimeoutRef.current = setTimeout(() => {
|
||||
tabFocusTimeoutRef.current = null;
|
||||
// If the shell produced output since the tab press, we assume it handled the tab
|
||||
// (e.g. autocomplete) so we should not toggle focus.
|
||||
if (lastOutputTimeRef.current > now) {
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
return;
|
||||
}
|
||||
setEmbeddedShellFocused(false);
|
||||
}, 100);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
|
||||
// Not idle, just focus it
|
||||
setEmbeddedShellFocused(true);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
||||
if (activePtyId) {
|
||||
backgroundCurrentShell();
|
||||
// After backgrounding, we explicitly do NOT show or focus the background UI.
|
||||
} else {
|
||||
if (isBackgroundShellVisible && !embeddedShellFocused) {
|
||||
setEmbeddedShellFocused(true);
|
||||
} else {
|
||||
toggleBackgroundShell();
|
||||
// Toggle focus based on intent: if we were hiding, unfocus; if showing, focus.
|
||||
if (!isBackgroundShellVisible && backgroundShells.size > 0) {
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundShells.size > 1) {
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
} else {
|
||||
setEmbeddedShellFocused(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
if (backgroundShells.size > 0 && isBackgroundShellVisible) {
|
||||
if (!embeddedShellFocused) {
|
||||
setEmbeddedShellFocused(true);
|
||||
}
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1595,11 +1702,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setCopyModeEnabled,
|
||||
copyModeEnabled,
|
||||
isAlternateBuffer,
|
||||
backgroundCurrentShell,
|
||||
toggleBackgroundShell,
|
||||
backgroundShells,
|
||||
isBackgroundShellVisible,
|
||||
setIsBackgroundShellListOpen,
|
||||
lastOutputTimeRef,
|
||||
tabFocusTimeoutRef,
|
||||
handleWarning,
|
||||
],
|
||||
);
|
||||
|
||||
useKeypress(handleGlobalKeypress, { isActive: true });
|
||||
useKeypress(handleGlobalKeypress, { isActive: true, priority: true });
|
||||
|
||||
useEffect(() => {
|
||||
// Respect hideWindowTitle settings
|
||||
@@ -1878,6 +1992,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isRestarting,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
embeddedShellFocused,
|
||||
showDebugProfiler,
|
||||
customDialog,
|
||||
@@ -1887,6 +2003,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
bannerVisible,
|
||||
terminalBackgroundColor: config.getTerminalBackground(),
|
||||
settingsNonce,
|
||||
backgroundShells,
|
||||
activeBackgroundShellPid,
|
||||
backgroundShellHeight,
|
||||
isBackgroundShellListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
}),
|
||||
@@ -1977,6 +2097,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
currentModel,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
historyManager,
|
||||
embeddedShellFocused,
|
||||
showDebugProfiler,
|
||||
@@ -1989,6 +2111,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
bannerVisible,
|
||||
config,
|
||||
settingsNonce,
|
||||
backgroundShellHeight,
|
||||
isBackgroundShellListOpen,
|
||||
activeBackgroundShellPid,
|
||||
backgroundShells,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
],
|
||||
@@ -2036,7 +2162,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleApiKeySubmit,
|
||||
handleApiKeyCancel,
|
||||
setBannerVisible,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
handleRestart: async () => {
|
||||
if (process.send) {
|
||||
@@ -2108,7 +2238,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleApiKeySubmit,
|
||||
handleApiKeyCancel,
|
||||
setBannerVisible,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
newAgents,
|
||||
config,
|
||||
|
||||
Reference in New Issue
Block a user