mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-04 14:11:11 -07:00
feat(cli): apply MRU active extension context with UI transparency
This commit is contained in:
@@ -175,6 +175,13 @@ export type SlashCommandActionReturn =
|
||||
| OpenCustomDialogActionReturn
|
||||
| LogoutActionReturn;
|
||||
|
||||
export enum CommandSource {
|
||||
CORE = 'core',
|
||||
EXTENSION = 'extension',
|
||||
USER = 'user',
|
||||
WORKSPACE = 'workspace',
|
||||
}
|
||||
|
||||
export enum CommandKind {
|
||||
BUILT_IN = 'built-in',
|
||||
USER_FILE = 'user-file',
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Command } from '../key/keyMatchers.js';
|
||||
import { formatCommand } from '../key/keybindingUtils.js';
|
||||
|
||||
interface ContextSummaryDisplayProps {
|
||||
activeExtensionName?: string;
|
||||
geminiMdFileCount: number;
|
||||
contextFileNames: string[];
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
@@ -29,6 +30,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
ideContext,
|
||||
skillCount,
|
||||
backgroundProcessCount = 0,
|
||||
activeExtensionName,
|
||||
}) => {
|
||||
const mcpServerCount = Object.keys(mcpServers || {}).length;
|
||||
const blockedMcpServerCount = blockedMcpServers?.length || 0;
|
||||
@@ -40,7 +42,8 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
blockedMcpServerCount === 0 &&
|
||||
openFileCount === 0 &&
|
||||
skillCount === 0 &&
|
||||
backgroundProcessCount === 0
|
||||
backgroundProcessCount === 0 &&
|
||||
!activeExtensionName
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -103,12 +106,20 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
}`;
|
||||
})();
|
||||
|
||||
const extensionText = (() => {
|
||||
if (!activeExtensionName) {
|
||||
return '';
|
||||
}
|
||||
return `Extension: ${activeExtensionName}`;
|
||||
})();
|
||||
|
||||
const summaryParts = [
|
||||
openFilesText,
|
||||
geminiMdText,
|
||||
mcpText,
|
||||
skillText,
|
||||
backgroundText,
|
||||
extensionText,
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,6 +39,7 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
|
||||
}
|
||||
skillCount={config.getSkillManager().getDisplayableSkills().length}
|
||||
backgroundProcessCount={uiState.backgroundTaskCount}
|
||||
activeExtensionName={config.activeExtensionName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,12 @@ import type {
|
||||
} from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { type CommandContext, type SlashCommand } from '../commands/types.js';
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
CommandSource,
|
||||
CommandKind,
|
||||
} from '../commands/types.js';
|
||||
import { CommandService } from '../../services/CommandService.js';
|
||||
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
|
||||
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
|
||||
@@ -92,6 +97,43 @@ interface SlashCommandProcessorActions {
|
||||
/**
|
||||
* Hook to define and process slash commands (e.g., /help, /clear).
|
||||
*/
|
||||
|
||||
function getCommandSource(command: SlashCommand): CommandSource {
|
||||
if (
|
||||
command.extensionName ||
|
||||
command.kind === CommandKind.EXTENSION_FILE ||
|
||||
command.kind === CommandKind.MCP_PROMPT ||
|
||||
command.kind === CommandKind.SKILL
|
||||
)
|
||||
return CommandSource.EXTENSION;
|
||||
if (command.kind === CommandKind.WORKSPACE_FILE)
|
||||
return CommandSource.WORKSPACE;
|
||||
if (command.kind === CommandKind.USER_FILE) return CommandSource.USER;
|
||||
return CommandSource.CORE;
|
||||
}
|
||||
|
||||
function shouldClearExtensionMRU(command: SlashCommand | undefined): boolean {
|
||||
if (!command) return false;
|
||||
|
||||
// Explicitly reset commands
|
||||
if (command.name === 'clear' || command.name === 'extension reset')
|
||||
return true;
|
||||
|
||||
// Exempt informational commands
|
||||
const exemptions = [
|
||||
'help',
|
||||
'settings',
|
||||
'status',
|
||||
'history',
|
||||
'bug',
|
||||
'exit',
|
||||
'quit',
|
||||
];
|
||||
if (exemptions.includes(command.name)) return false;
|
||||
|
||||
return getCommandSource(command) === CommandSource.CORE;
|
||||
}
|
||||
|
||||
export const useSlashCommandProcessor = (
|
||||
config: Config | null,
|
||||
settings: LoadedSettings,
|
||||
@@ -449,7 +491,8 @@ export const useSlashCommandProcessor = (
|
||||
toolArgs: result.toolArgs,
|
||||
postSubmitPrompt: result.postSubmitPrompt,
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'message':
|
||||
addItem(
|
||||
@@ -465,7 +508,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'logout':
|
||||
// Show logout confirmation dialog with Login/Exit options
|
||||
@@ -485,7 +529,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'dialog':
|
||||
switch (result.dialog) {
|
||||
@@ -494,49 +539,56 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'theme':
|
||||
actions.openThemeDialog();
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'editor':
|
||||
actions.openEditorDialog();
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'privacy':
|
||||
actions.openPrivacyNotice();
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'sessionBrowser':
|
||||
actions.openSessionBrowser();
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'settings':
|
||||
actions.openSettingsDialog();
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'model':
|
||||
actions.openModelDialog();
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'agentConfig': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
@@ -563,7 +615,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
}
|
||||
case 'permissions':
|
||||
@@ -574,13 +627,15 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'help':
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
default: {
|
||||
const unhandled: never = result.dialog;
|
||||
@@ -598,7 +653,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
}
|
||||
case 'quit':
|
||||
@@ -606,7 +662,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
|
||||
case 'submit_prompt':
|
||||
@@ -614,7 +671,8 @@ export const useSlashCommandProcessor = (
|
||||
type: 'submit_prompt',
|
||||
content: result.content,
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
case 'confirm_shell_commands': {
|
||||
const callId = `expansion-${Date.now()}`;
|
||||
@@ -674,7 +732,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -716,7 +775,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -731,7 +791,8 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU:
|
||||
shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
@@ -746,7 +807,7 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU: shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
} else if (commandToExecute.subCommands) {
|
||||
const helpText = `Command '/${commandToExecute.name}' requires a subcommand. Available:\n${commandToExecute.subCommands
|
||||
@@ -760,7 +821,7 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU: shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -768,7 +829,7 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU: shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
hasError = true;
|
||||
@@ -791,7 +852,7 @@ export const useSlashCommandProcessor = (
|
||||
return {
|
||||
type: 'handled',
|
||||
activeExtensionName: commandToExecute?.extensionName,
|
||||
clearExtensionMRU: commandToExecute?.name === 'clear',
|
||||
clearExtensionMRU: shouldClearExtensionMRU(commandToExecute),
|
||||
};
|
||||
} finally {
|
||||
if (config && resolvedCommandPath[0] && !hasError) {
|
||||
|
||||
@@ -100,6 +100,7 @@ const MockedGeminiClientClass = vi.hoisted(() =>
|
||||
recordMessageTokens: vi.fn(),
|
||||
recordToolCalls: vi.fn(),
|
||||
getConversationFile: vi.fn(),
|
||||
recordActiveExtensionName: vi.fn(),
|
||||
});
|
||||
this.getCurrentSequenceModel = vi
|
||||
.fn()
|
||||
@@ -310,6 +311,8 @@ describe('useGeminiStream', () => {
|
||||
debugMode: false,
|
||||
question: undefined,
|
||||
coreTools: [],
|
||||
activeExtensionName: undefined,
|
||||
setActiveExtensionName: vi.fn(),
|
||||
toolDiscoveryCommand: undefined,
|
||||
toolCallCommand: undefined,
|
||||
mcpServerCommand: undefined,
|
||||
|
||||
@@ -937,10 +937,20 @@ export const useGeminiStream = (
|
||||
if (slashCommandResult) {
|
||||
if (slashCommandResult.clearExtensionMRU) {
|
||||
config.setActiveExtensionName(undefined);
|
||||
config
|
||||
.getGeminiClient()
|
||||
?.getChatRecordingService()
|
||||
?.recordActiveExtensionName(undefined);
|
||||
} else if (slashCommandResult.activeExtensionName) {
|
||||
config.setActiveExtensionName(
|
||||
slashCommandResult.activeExtensionName,
|
||||
);
|
||||
config
|
||||
.getGeminiClient()
|
||||
?.getChatRecordingService()
|
||||
?.recordActiveExtensionName(
|
||||
slashCommandResult.activeExtensionName,
|
||||
);
|
||||
}
|
||||
switch (slashCommandResult.type) {
|
||||
case 'schedule_tool': {
|
||||
@@ -1762,6 +1772,10 @@ export const useGeminiStream = (
|
||||
newApprovalMode !== ApprovalMode.PLAN
|
||||
) {
|
||||
config.setActiveExtensionName(undefined);
|
||||
config
|
||||
.getGeminiClient()
|
||||
?.getChatRecordingService()
|
||||
?.recordActiveExtensionName(undefined);
|
||||
}
|
||||
if (
|
||||
previousApprovalModeRef.current === ApprovalMode.PLAN &&
|
||||
|
||||
@@ -83,6 +83,13 @@ export function useSessionResume({
|
||||
workspaceContext.addDirectories(resumedData.conversation.directories);
|
||||
}
|
||||
|
||||
// Restore active extension context
|
||||
if (resumedData.conversation.activeExtensionName) {
|
||||
config.setActiveExtensionName(
|
||||
resumedData.conversation.activeExtensionName,
|
||||
);
|
||||
}
|
||||
|
||||
// Give the history to the Gemini client.
|
||||
await config.getGeminiClient()?.resumeChat(clientHistory, resumedData);
|
||||
} catch (error) {
|
||||
|
||||
@@ -603,6 +603,18 @@ export class ChatRecordingService {
|
||||
}
|
||||
}
|
||||
|
||||
recordActiveExtensionName(activeExtensionName: string | undefined): void {
|
||||
if (!this.conversationFile) return;
|
||||
try {
|
||||
this.updateMetadata({ activeExtensionName });
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
'Error saving active extension to chat history.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getConversation(): ConversationRecord | null {
|
||||
if (!this.conversationFile) return null;
|
||||
return this.cachedConversation;
|
||||
|
||||
@@ -87,6 +87,8 @@ export interface ConversationRecord {
|
||||
directories?: string[];
|
||||
/** The kind of conversation (main agent or subagent) */
|
||||
kind?: 'main' | 'subagent';
|
||||
/** The last active extension context for the session */
|
||||
activeExtensionName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user