feat(ui): implement refreshed UX for Composer layout

- Promotes refreshed multi-row status area and footer as the default experience.
- Stabilizes Composer row heights to prevent layout 'jitter' during typing and model turns.
- Unifies active hook status and model loading indicators into a single, stable Row 1.
- Refactors settings to use backward-compatible 'Hide' booleans (ui.hideStatusTips, ui.hideStatusWit).
- Removes vestigial context usage bleed-through logic in minimal mode to align with global UX direction.
- Relocates toast notifications to the top status row for improved visibility.
- Updates all CLI UI snapshots and architectural tests to reflect the stabilized layout.
This commit is contained in:
Keith Guerin
2026-03-17 15:00:53 -07:00
parent ff196fbe6f
commit 576eaff9cd
371 changed files with 4713 additions and 14249 deletions

View File

@@ -26,7 +26,7 @@ describe('Task Event-Driven Scheduler', () => {
mockConfig = createMockConfig({
isEventDrivenSchedulerEnabled: () => true,
}) as Config;
messageBus = mockConfig.messageBus;
messageBus = mockConfig.getMessageBus();
mockEventBus = {
publish: vi.fn(),
on: vi.fn(),
@@ -360,7 +360,7 @@ describe('Task Event-Driven Scheduler', () => {
isEventDrivenSchedulerEnabled: () => true,
getApprovalMode: () => ApprovalMode.YOLO,
}) as Config;
const yoloMessageBus = yoloConfig.messageBus;
const yoloMessageBus = yoloConfig.getMessageBus();
// @ts-expect-error - Calling private constructor
const task = new Task('task-id', 'context-id', yoloConfig, mockEventBus);

View File

@@ -5,7 +5,6 @@
*/
import {
type AgentLoopContext,
Scheduler,
type GeminiClient,
GeminiEventType,
@@ -115,8 +114,7 @@ export class Task {
this.scheduler = this.setupEventDrivenScheduler();
const loopContext: AgentLoopContext = this.config;
this.geminiClient = loopContext.geminiClient;
this.geminiClient = this.config.getGeminiClient();
this.pendingToolConfirmationDetails = new Map();
this.taskState = 'submitted';
this.eventBus = eventBus;
@@ -145,8 +143,7 @@ export class Task {
// process. This is not scoped to the individual task but reflects the global connection
// state managed within the @gemini-cli/core module.
async getMetadata(): Promise<TaskMetadata> {
const loopContext: AgentLoopContext = this.config;
const toolRegistry = loopContext.toolRegistry;
const toolRegistry = this.config.getToolRegistry();
const mcpServers = this.config.getMcpClientManager()?.getMcpServers() || {};
const serverStatuses = getAllMCPServerStatuses();
const servers = Object.keys(mcpServers).map((serverName) => ({
@@ -379,8 +376,7 @@ export class Task {
private messageBusListener?: (message: ToolCallsUpdateMessage) => void;
private setupEventDrivenScheduler(): Scheduler {
const loopContext: AgentLoopContext = this.config;
const messageBus = loopContext.messageBus;
const messageBus = this.config.getMessageBus();
const scheduler = new Scheduler({
schedulerId: this.id,
context: this.config,
@@ -399,11 +395,9 @@ export class Task {
dispose(): void {
if (this.messageBusListener) {
const loopContext: AgentLoopContext = this.config;
loopContext.messageBus.unsubscribe(
MessageBusType.TOOL_CALLS_UPDATE,
this.messageBusListener,
);
this.config
.getMessageBus()
.unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, this.messageBusListener);
this.messageBusListener = undefined;
}
@@ -954,8 +948,7 @@ export class Task {
try {
if (correlationId) {
const loopContext: AgentLoopContext = this.config;
await loopContext.messageBus.publish({
await this.config.getMessageBus().publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId,
confirmed:

View File

@@ -59,9 +59,6 @@ describe('a2a-server memory commands', () => {
} as unknown as ToolRegistry;
mockConfig = {
get toolRegistry() {
return mockToolRegistry;
},
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
} as unknown as Config;
@@ -171,19 +168,17 @@ describe('a2a-server memory commands', () => {
]);
expect(mockAddMemory).toHaveBeenCalledWith(fact);
expect(mockConfig.getToolRegistry).toHaveBeenCalled();
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('save_memory');
expect(mockSaveMemoryTool.buildAndExecute).toHaveBeenCalledWith(
{ fact },
expect.any(AbortSignal),
undefined,
{
shellExecutionConfig: {
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
sandboxManager: undefined,
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
},
);

View File

@@ -15,7 +15,6 @@ import type {
CommandContext,
CommandExecutionResponse,
} from './types.js';
import type { AgentLoopContext } from '@google/gemini-cli-core';
const DEFAULT_SANITIZATION_CONFIG = {
allowedEnvironmentVariables: [],
@@ -96,17 +95,13 @@ export class AddMemoryCommand implements Command {
return { name: this.name, data: result.content };
}
const loopContext: AgentLoopContext = context.config;
const toolRegistry = loopContext.toolRegistry;
const toolRegistry = context.config.getToolRegistry();
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
const signal = abortController.signal;
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: loopContext.sandboxManager,
},
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
});
await refreshMemory(context.config);
return {

View File

@@ -16,14 +16,11 @@ import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
GeminiClient,
HookSystem,
type MessageBus,
PolicyDecision,
tmpdir,
type Config,
type Storage,
NoopSandboxManager,
type ToolRegistry,
type SandboxManager,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
import { expect, vi } from 'vitest';
@@ -34,27 +31,9 @@ export function createMockConfig(
const tmpDir = tmpdir();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const mockConfig = {
get config() {
get toolRegistry(): ToolRegistry {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return this as unknown as Config;
},
get toolRegistry() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = this as unknown as Config;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return config.getToolRegistry?.() as unknown as ToolRegistry;
},
get messageBus() {
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(this as unknown as Config).getMessageBus?.() as unknown as MessageBus
);
},
get geminiClient() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = this as unknown as Config;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return config.getGeminiClient?.() as unknown as GeminiClient;
return (this as unknown as Config).getToolRegistry();
},
getToolRegistry: vi.fn().mockReturnValue({
getTool: vi.fn(),
@@ -99,18 +78,12 @@ export function createMockConfig(
}),
getGitService: vi.fn(),
validatePathAccess: vi.fn().mockReturnValue(undefined),
getShellExecutionConfig: vi.fn().mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
sandboxManager: new NoopSandboxManager() as unknown as SandboxManager,
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
}),
...overrides,
} as unknown as Config;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(mockConfig as unknown as { config: Config; promptId: string }).config =
mockConfig;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(mockConfig as unknown as { config: Config; promptId: string }).promptId =
'test-prompt-id';