feat(cli): apply MRU active extension context with UI transparency

This commit is contained in:
Mahima Shanware
2026-04-14 18:34:54 +00:00
parent dfb641c2e9
commit 04741d1d73
9 changed files with 143 additions and 25 deletions
+7
View File
@@ -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;
}
/**