mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 13:11:03 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb1ff6eb20 | |||
| ade6dc0cc7 |
@@ -16,7 +16,11 @@ import {
|
|||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { handleInstall, installCommand } from './install.js';
|
import { handleInstall, installCommand } from './install.js';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import * as core from '@google/gemini-cli-core';
|
import {
|
||||||
|
debugLogger,
|
||||||
|
type FolderTrustDiscoveryService,
|
||||||
|
type GeminiCLIExtension,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
ExtensionManager,
|
ExtensionManager,
|
||||||
type inferInstallMetadata,
|
type inferInstallMetadata,
|
||||||
@@ -52,7 +56,7 @@ const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() =>
|
|||||||
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
|
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
|
||||||
vi.fn(),
|
vi.fn(),
|
||||||
);
|
);
|
||||||
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> =
|
const mockDiscover: Mock<typeof FolderTrustDiscoveryService.discover> =
|
||||||
vi.hoisted(() => vi.fn());
|
vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock('../../config/extensions/consent.js', () => ({
|
vi.mock('../../config/extensions/consent.js', () => ({
|
||||||
@@ -117,8 +121,8 @@ describe('handleInstall', () => {
|
|||||||
let processSpy: MockInstance;
|
let processSpy: MockInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
debugLogSpy = vi.spyOn(core.debugLogger, 'log');
|
debugLogSpy = vi.spyOn(debugLogger, 'log');
|
||||||
debugErrorSpy = vi.spyOn(core.debugLogger, 'error');
|
debugErrorSpy = vi.spyOn(debugLogger, 'error');
|
||||||
processSpy = vi
|
processSpy = vi
|
||||||
.spyOn(process, 'exit')
|
.spyOn(process, 'exit')
|
||||||
.mockImplementation(() => undefined as never);
|
.mockImplementation(() => undefined as never);
|
||||||
@@ -171,8 +175,8 @@ describe('handleInstall', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function createMockExtension(
|
function createMockExtension(
|
||||||
overrides: Partial<core.GeminiCLIExtension> = {},
|
overrides: Partial<GeminiCLIExtension> = {},
|
||||||
): core.GeminiCLIExtension {
|
): GeminiCLIExtension {
|
||||||
return {
|
return {
|
||||||
name: 'mock-extension',
|
name: 'mock-extension',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ import {
|
|||||||
type MCPServerConfig,
|
type MCPServerConfig,
|
||||||
type GeminiCLIExtension,
|
type GeminiCLIExtension,
|
||||||
Storage,
|
Storage,
|
||||||
|
PolicyDecision,
|
||||||
|
TelemetryTarget,
|
||||||
|
loadServerHierarchicalMemory,
|
||||||
|
createPolicyEngineConfig,
|
||||||
|
type Config as CoreConfig,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +33,6 @@ import {
|
|||||||
type MergedSettings,
|
type MergedSettings,
|
||||||
createTestMergedSettings,
|
createTestMergedSettings,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
import * as ServerConfig from '@google/gemini-cli-core';
|
|
||||||
|
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
import { ExtensionManager } from './extension-manager.js';
|
import { ExtensionManager } from './extension-manager.js';
|
||||||
@@ -99,9 +103,9 @@ vi.mock('read-package-up', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@google/gemini-cli-core', async () => {
|
vi.mock('@google/gemini-cli-core', async () => {
|
||||||
const actualServer = await vi.importActual<typeof ServerConfig>(
|
const actualServer = await vi.importActual<
|
||||||
'@google/gemini-cli-core',
|
typeof import('@google/gemini-cli-core')
|
||||||
);
|
>('@google/gemini-cli-core');
|
||||||
return {
|
return {
|
||||||
...actualServer,
|
...actualServer,
|
||||||
IdeClient: {
|
IdeClient: {
|
||||||
@@ -146,8 +150,8 @@ vi.mock('@google/gemini-cli-core', async () => {
|
|||||||
createPolicyEngineConfig: vi.fn(async () => ({
|
createPolicyEngineConfig: vi.fn(async () => ({
|
||||||
rules: [],
|
rules: [],
|
||||||
checkers: [],
|
checkers: [],
|
||||||
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
|
defaultDecision: PolicyDecision.ASK_USER,
|
||||||
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
|
approvalMode: ApprovalMode.DEFAULT,
|
||||||
})),
|
})),
|
||||||
getAdminErrorMessage: vi.fn(
|
getAdminErrorMessage: vi.fn(
|
||||||
(_feature) =>
|
(_feature) =>
|
||||||
@@ -846,7 +850,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
]);
|
]);
|
||||||
const argv = await parseArguments(createTestMergedSettings());
|
const argv = await parseArguments(createTestMergedSettings());
|
||||||
await loadCliConfig(settings, 'session-id', argv);
|
await loadCliConfig(settings, 'session-id', argv);
|
||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[],
|
[],
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -874,7 +878,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
await loadCliConfig(settings, 'session-id', argv);
|
await loadCliConfig(settings, 'session-id', argv);
|
||||||
|
|
||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[includeDir],
|
[includeDir],
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -901,7 +905,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
await loadCliConfig(settings, 'session-id', argv);
|
await loadCliConfig(settings, 'session-id', argv);
|
||||||
|
|
||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[],
|
[],
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -2500,7 +2504,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set YOLO approval mode when --yolo flag is used', async () => {
|
it('should set YOLO approval mode when --yolo flag is used', async () => {
|
||||||
@@ -2511,7 +2515,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set YOLO approval mode when -y flag is used', async () => {
|
it('should set YOLO approval mode when -y flag is used', async () => {
|
||||||
@@ -2522,7 +2526,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
|
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
|
||||||
@@ -2533,7 +2537,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
|
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
|
||||||
@@ -2544,7 +2548,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
|
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
|
||||||
@@ -2555,7 +2559,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => {
|
it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => {
|
||||||
@@ -2570,7 +2574,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
|
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
|
||||||
@@ -2581,7 +2585,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => {
|
it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => {
|
||||||
@@ -2593,7 +2597,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
|
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
|
||||||
@@ -2606,7 +2610,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
});
|
});
|
||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
|
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
|
||||||
@@ -2663,7 +2667,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override --approval-mode=auto_edit to DEFAULT', async () => {
|
it('should override --approval-mode=auto_edit to DEFAULT', async () => {
|
||||||
@@ -2674,7 +2678,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override --yolo flag to DEFAULT', async () => {
|
it('should override --yolo flag to DEFAULT', async () => {
|
||||||
@@ -2685,7 +2689,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remain DEFAULT when --approval-mode=default', async () => {
|
it('should remain DEFAULT when --approval-mode=default', async () => {
|
||||||
@@ -2696,7 +2700,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
'test-session',
|
'test-session',
|
||||||
argv,
|
argv,
|
||||||
);
|
);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2708,9 +2712,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
});
|
});
|
||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(
|
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
ServerConfig.ApprovalMode.AUTO_EDIT,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prioritize --approval-mode flag over settings', async () => {
|
it('should prioritize --approval-mode flag over settings', async () => {
|
||||||
@@ -2720,9 +2722,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
});
|
});
|
||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(
|
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
|
||||||
ServerConfig.ApprovalMode.AUTO_EDIT,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prioritize --yolo flag over settings', async () => {
|
it('should prioritize --yolo flag over settings', async () => {
|
||||||
@@ -2732,7 +2732,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
});
|
});
|
||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
|
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
|
||||||
@@ -2743,7 +2743,7 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
});
|
});
|
||||||
const argv = await parseArguments(settings);
|
const argv = await parseArguments(settings);
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
|
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
|
||||||
@@ -2841,7 +2841,7 @@ describe('loadCliConfig fileFiltering', () => {
|
|||||||
>;
|
>;
|
||||||
const testCases: Array<{
|
const testCases: Array<{
|
||||||
property: keyof FileFilteringSettings;
|
property: keyof FileFilteringSettings;
|
||||||
getter: (config: ServerConfig.Config) => boolean;
|
getter: (config: CoreConfig) => boolean;
|
||||||
value: boolean;
|
value: boolean;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
@@ -3085,7 +3085,7 @@ describe('Telemetry configuration via environment variables', () => {
|
|||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments(createTestMergedSettings());
|
const argv = await parseArguments(createTestMergedSettings());
|
||||||
const settings = createTestMergedSettings({
|
const settings = createTestMergedSettings({
|
||||||
telemetry: { target: ServerConfig.TelemetryTarget.LOCAL },
|
telemetry: { target: TelemetryTarget.LOCAL },
|
||||||
});
|
});
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getTelemetryTarget()).toBe('gcp');
|
expect(config.getTelemetryTarget()).toBe('gcp');
|
||||||
@@ -3096,7 +3096,7 @@ describe('Telemetry configuration via environment variables', () => {
|
|||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments(createTestMergedSettings());
|
const argv = await parseArguments(createTestMergedSettings());
|
||||||
const settings = createTestMergedSettings({
|
const settings = createTestMergedSettings({
|
||||||
telemetry: { target: ServerConfig.TelemetryTarget.GCP },
|
telemetry: { target: TelemetryTarget.GCP },
|
||||||
});
|
});
|
||||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||||
/Invalid telemetry configuration: .*Invalid telemetry target/i,
|
/Invalid telemetry configuration: .*Invalid telemetry target/i,
|
||||||
@@ -3174,7 +3174,7 @@ describe('Telemetry configuration via environment variables', () => {
|
|||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments(createTestMergedSettings());
|
const argv = await parseArguments(createTestMergedSettings());
|
||||||
const settings = createTestMergedSettings({
|
const settings = createTestMergedSettings({
|
||||||
telemetry: { target: ServerConfig.TelemetryTarget.LOCAL },
|
telemetry: { target: TelemetryTarget.LOCAL },
|
||||||
});
|
});
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
expect(config.getTelemetryTarget()).toBe('local');
|
expect(config.getTelemetryTarget()).toBe('local');
|
||||||
@@ -3298,7 +3298,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
|
|||||||
|
|
||||||
await loadCliConfig(settings, 'test-session', argv);
|
await loadCliConfig(settings, 'test-session', argv);
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tools: expect.objectContaining({
|
tools: expect.objectContaining({
|
||||||
allowed: expect.arrayContaining(['cli-tool']),
|
allowed: expect.arrayContaining(['cli-tool']),
|
||||||
@@ -3319,7 +3319,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
|
|||||||
await loadCliConfig(settings, 'test-session', argv);
|
await loadCliConfig(settings, 'test-session', argv);
|
||||||
|
|
||||||
// In non-interactive mode, only ask_user is excluded by default
|
// In non-interactive mode, only ask_user is excluded by default
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
tools: expect.objectContaining({
|
tools: expect.objectContaining({
|
||||||
exclude: expect.arrayContaining([ASK_USER_TOOL_NAME]),
|
exclude: expect.arrayContaining([ASK_USER_TOOL_NAME]),
|
||||||
@@ -3341,7 +3341,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
|
|||||||
|
|
||||||
await loadCliConfig(settings, 'test-session', argv);
|
await loadCliConfig(settings, 'test-session', argv);
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
|
policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface CliArgs {
|
|||||||
rawOutput: boolean | undefined;
|
rawOutput: boolean | undefined;
|
||||||
acceptRawOutputRisk: boolean | undefined;
|
acceptRawOutputRisk: boolean | undefined;
|
||||||
isCommand: boolean | undefined;
|
isCommand: boolean | undefined;
|
||||||
|
fullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,6 +298,10 @@ export async function parseArguments(
|
|||||||
.option('accept-raw-output-risk', {
|
.option('accept-raw-output-risk', {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Suppress the security warning when using --raw-output.',
|
description: 'Suppress the security warning when using --raw-output.',
|
||||||
|
})
|
||||||
|
.option('fullscreen', {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Enable experimental fullscreen mode.',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// Register MCP subcommands
|
// Register MCP subcommands
|
||||||
@@ -435,6 +440,10 @@ export async function loadCliConfig(
|
|||||||
process.env['GEMINI_SANDBOX'] = 'true';
|
process.env['GEMINI_SANDBOX'] = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (argv.fullscreen !== undefined) {
|
||||||
|
settings.experimental.fullscreen = argv.fullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
||||||
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
|
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
|
||||||
|
|
||||||
|
|||||||
@@ -1928,6 +1928,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
description: 'Enable Plan Mode.',
|
description: 'Enable Plan Mode.',
|
||||||
showInDialog: true,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
|
fullscreen: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Fullscreen Mode',
|
||||||
|
category: 'Experimental',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Enable experimental fullscreen mode for integrated and background shells (toggle with ctrl+7).',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
taskTracker: {
|
taskTracker: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Task Tracker',
|
label: 'Task Tracker',
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { loadCliConfig, type CliArgs } from './config.js';
|
import { loadCliConfig, type CliArgs } from './config.js';
|
||||||
import { createTestMergedSettings } from './settings.js';
|
import { createTestMergedSettings } from './settings.js';
|
||||||
import * as ServerConfig from '@google/gemini-cli-core';
|
import {
|
||||||
|
isHeadlessMode,
|
||||||
|
Storage,
|
||||||
|
createPolicyEngineConfig,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
import * as Policy from './policy.js';
|
import * as Policy from './policy.js';
|
||||||
|
|
||||||
@@ -21,9 +25,9 @@ const mockCheckIntegrity = vi.fn();
|
|||||||
const mockAcceptIntegrity = vi.fn();
|
const mockAcceptIntegrity = vi.fn();
|
||||||
|
|
||||||
vi.mock('@google/gemini-cli-core', async () => {
|
vi.mock('@google/gemini-cli-core', async () => {
|
||||||
const actual = await vi.importActual<typeof ServerConfig>(
|
const actual = await vi.importActual<
|
||||||
'@google/gemini-cli-core',
|
typeof import('@google/gemini-cli-core')
|
||||||
);
|
>('@google/gemini-cli-core');
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
|
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
|
||||||
@@ -61,11 +65,11 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
hash: 'test-hash',
|
hash: 'test-hash',
|
||||||
fileCount: 1,
|
fileCount: 1,
|
||||||
});
|
});
|
||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
|
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have getWorkspacePoliciesDir on Storage class', () => {
|
it('should have getWorkspacePoliciesDir on Storage class', () => {
|
||||||
const storage = new ServerConfig.Storage(MOCK_CWD);
|
const storage = new Storage(MOCK_CWD);
|
||||||
expect(storage.getWorkspacePoliciesDir).toBeDefined();
|
expect(storage.getWorkspacePoliciesDir).toBeDefined();
|
||||||
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
|
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
|
||||||
});
|
});
|
||||||
@@ -81,7 +85,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
|
|
||||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: expect.stringContaining(
|
workspacePoliciesDir: expect.stringContaining(
|
||||||
path.join('.gemini', 'policies'),
|
path.join('.gemini', 'policies'),
|
||||||
@@ -102,7 +106,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
|
|
||||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: undefined,
|
workspacePoliciesDir: undefined,
|
||||||
}),
|
}),
|
||||||
@@ -126,7 +130,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
|
|
||||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: undefined,
|
workspacePoliciesDir: undefined,
|
||||||
}),
|
}),
|
||||||
@@ -144,7 +148,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
hash: 'new-hash',
|
hash: 'new-hash',
|
||||||
fileCount: 1,
|
fileCount: 1,
|
||||||
});
|
});
|
||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(true); // Non-interactive
|
vi.mocked(isHeadlessMode).mockReturnValue(true); // Non-interactive
|
||||||
|
|
||||||
const settings = createTestMergedSettings();
|
const settings = createTestMergedSettings();
|
||||||
const argv = { prompt: 'do something' } as unknown as CliArgs;
|
const argv = { prompt: 'do something' } as unknown as CliArgs;
|
||||||
@@ -156,7 +160,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
MOCK_CWD,
|
MOCK_CWD,
|
||||||
'new-hash',
|
'new-hash',
|
||||||
);
|
);
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: expect.stringContaining(
|
workspacePoliciesDir: expect.stringContaining(
|
||||||
path.join('.gemini', 'policies'),
|
path.join('.gemini', 'policies'),
|
||||||
@@ -176,7 +180,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
hash: 'new-hash',
|
hash: 'new-hash',
|
||||||
fileCount: 1,
|
fileCount: 1,
|
||||||
});
|
});
|
||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
|
||||||
|
|
||||||
const settings = createTestMergedSettings();
|
const settings = createTestMergedSettings();
|
||||||
const argv = {
|
const argv = {
|
||||||
@@ -194,7 +198,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
MOCK_CWD,
|
MOCK_CWD,
|
||||||
'new-hash',
|
'new-hash',
|
||||||
);
|
);
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: expect.stringContaining(
|
workspacePoliciesDir: expect.stringContaining(
|
||||||
path.join('.gemini', 'policies'),
|
path.join('.gemini', 'policies'),
|
||||||
@@ -214,7 +218,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
hash: 'new-hash',
|
hash: 'new-hash',
|
||||||
fileCount: 5,
|
fileCount: 5,
|
||||||
});
|
});
|
||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
|
||||||
|
|
||||||
const settings = createTestMergedSettings();
|
const settings = createTestMergedSettings();
|
||||||
const argv = { query: 'test' } as unknown as CliArgs;
|
const argv = { query: 'test' } as unknown as CliArgs;
|
||||||
@@ -230,7 +234,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
'new-hash',
|
'new-hash',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: expect.stringContaining(
|
workspacePoliciesDir: expect.stringContaining(
|
||||||
path.join('.gemini', 'policies'),
|
path.join('.gemini', 'policies'),
|
||||||
@@ -255,7 +259,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
hash: 'new-hash',
|
hash: 'new-hash',
|
||||||
fileCount: 1,
|
fileCount: 1,
|
||||||
});
|
});
|
||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
|
||||||
|
|
||||||
const settings = createTestMergedSettings();
|
const settings = createTestMergedSettings();
|
||||||
const argv = {
|
const argv = {
|
||||||
@@ -273,7 +277,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
||||||
newHash: 'new-hash',
|
newHash: 'new-hash',
|
||||||
});
|
});
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: undefined,
|
workspacePoliciesDir: undefined,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1148,6 +1148,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
activeBackgroundShellPid,
|
activeBackgroundShellPid,
|
||||||
setIsBackgroundShellListOpen,
|
setIsBackgroundShellListOpen,
|
||||||
isBackgroundShellListOpen,
|
isBackgroundShellListOpen,
|
||||||
|
isBackgroundShellFullscreen,
|
||||||
|
setIsBackgroundShellFullscreen,
|
||||||
setActiveBackgroundShellPid,
|
setActiveBackgroundShellPid,
|
||||||
backgroundShellHeight,
|
backgroundShellHeight,
|
||||||
} = useBackgroundShellManager({
|
} = useBackgroundShellManager({
|
||||||
@@ -1160,6 +1162,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
terminalHeight,
|
terminalHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [isForegroundShellFullscreen, setIsForegroundShellFullscreen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
|
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
|
||||||
|
|
||||||
const lastOutputTimeRef = useRef(0);
|
const lastOutputTimeRef = useRef(0);
|
||||||
@@ -1840,6 +1845,29 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
setIsBackgroundShellListOpen(true);
|
setIsBackgroundShellListOpen(true);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
} else if (keyMatchers[Command.TOGGLE_SHELL_FULLSCREEN](key)) {
|
||||||
|
if (
|
||||||
|
settings.merged.experimental.fullscreen &&
|
||||||
|
backgroundShells.size > 0 &&
|
||||||
|
isBackgroundShellVisible
|
||||||
|
) {
|
||||||
|
setIsBackgroundShellFullscreen((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
if (newValue) {
|
||||||
|
setEmbeddedShellFocused(true);
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
} else if (settings.merged.experimental.fullscreen && activePtyId) {
|
||||||
|
setIsForegroundShellFullscreen((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
if (newValue) {
|
||||||
|
setEmbeddedShellFocused(true);
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -1870,7 +1898,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
lastOutputTimeRef,
|
lastOutputTimeRef,
|
||||||
showTransientMessage,
|
showTransientMessage,
|
||||||
settings.merged.general.devtools,
|
settings.merged.general.devtools,
|
||||||
|
settings.merged.experimental.fullscreen,
|
||||||
showErrorDetails,
|
showErrorDetails,
|
||||||
|
setIsBackgroundShellFullscreen,
|
||||||
|
setIsForegroundShellFullscreen,
|
||||||
triggerExpandHint,
|
triggerExpandHint,
|
||||||
keyMatchers,
|
keyMatchers,
|
||||||
isHelpDismissKey,
|
isHelpDismissKey,
|
||||||
@@ -2298,6 +2329,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
backgroundShells,
|
backgroundShells,
|
||||||
activeBackgroundShellPid,
|
activeBackgroundShellPid,
|
||||||
backgroundShellHeight,
|
backgroundShellHeight,
|
||||||
|
isBackgroundShellFullscreen,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
isBackgroundShellListOpen,
|
isBackgroundShellListOpen,
|
||||||
adminSettingsChanged,
|
adminSettingsChanged,
|
||||||
newAgents,
|
newAgents,
|
||||||
@@ -2424,6 +2457,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
config,
|
config,
|
||||||
settingsNonce,
|
settingsNonce,
|
||||||
backgroundShellHeight,
|
backgroundShellHeight,
|
||||||
|
isBackgroundShellFullscreen,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
isBackgroundShellListOpen,
|
isBackgroundShellListOpen,
|
||||||
activeBackgroundShellPid,
|
activeBackgroundShellPid,
|
||||||
backgroundShells,
|
backgroundShells,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
type RadioSelectItem,
|
type RadioSelectItem,
|
||||||
} from './shared/RadioButtonSelect.js';
|
} from './shared/RadioButtonSelect.js';
|
||||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
interface BackgroundShellDisplayProps {
|
interface BackgroundShellDisplayProps {
|
||||||
shells: Map<number, BackgroundShell>;
|
shells: Map<number, BackgroundShell>;
|
||||||
@@ -70,6 +71,7 @@ export const BackgroundShellDisplay = ({
|
|||||||
isListOpenProp,
|
isListOpenProp,
|
||||||
}: BackgroundShellDisplayProps) => {
|
}: BackgroundShellDisplayProps) => {
|
||||||
const keyMatchers = useKeyMatchers();
|
const keyMatchers = useKeyMatchers();
|
||||||
|
const settings = useSettings();
|
||||||
const {
|
const {
|
||||||
dismissBackgroundShell,
|
dismissBackgroundShell,
|
||||||
setActiveBackgroundShellPid,
|
setActiveBackgroundShellPid,
|
||||||
@@ -178,6 +180,10 @@ export const BackgroundShellDisplay = ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyMatchers[Command.TOGGLE_SHELL_FULLSCREEN](key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
|
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
|
||||||
void dismissBackgroundShell(activeShell.pid);
|
void dismissBackgroundShell(activeShell.pid);
|
||||||
return true;
|
return true;
|
||||||
@@ -207,6 +213,9 @@ export const BackgroundShellDisplay = ({
|
|||||||
{ label: 'Close', command: Command.TOGGLE_BACKGROUND_SHELL },
|
{ label: 'Close', command: Command.TOGGLE_BACKGROUND_SHELL },
|
||||||
{ label: 'Kill', command: Command.KILL_BACKGROUND_SHELL },
|
{ label: 'Kill', command: Command.KILL_BACKGROUND_SHELL },
|
||||||
{ label: 'List', command: Command.TOGGLE_BACKGROUND_SHELL_LIST },
|
{ label: 'List', command: Command.TOGGLE_BACKGROUND_SHELL_LIST },
|
||||||
|
...(settings.merged.experimental.fullscreen
|
||||||
|
? [{ label: 'Fullscreen', command: Command.TOGGLE_SHELL_FULLSCREEN }]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const helpTextStr = helpTextParts
|
const helpTextStr = helpTextParts
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface HistoryItemDisplayProps {
|
|||||||
isExpandable?: boolean;
|
isExpandable?: boolean;
|
||||||
isFirstThinking?: boolean;
|
isFirstThinking?: boolean;
|
||||||
isFirstAfterThinking?: boolean;
|
isFirstAfterThinking?: boolean;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||||
@@ -60,6 +61,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
isExpandable,
|
isExpandable,
|
||||||
isFirstThinking = false,
|
isFirstThinking = false,
|
||||||
isFirstAfterThinking = false,
|
isFirstAfterThinking = false,
|
||||||
|
isFullscreen = false,
|
||||||
}) => {
|
}) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||||
@@ -73,7 +75,8 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
key={itemForDisplay.id}
|
key={itemForDisplay.id}
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
marginTop={needsTopMarginAfterThinking ? 1 : 0}
|
marginTop={isFullscreen ? 0 : needsTopMarginAfterThinking ? 1 : 0}
|
||||||
|
paddingTop={isFullscreen ? 1 : 0}
|
||||||
>
|
>
|
||||||
{/* Render standard message types */}
|
{/* Render standard message types */}
|
||||||
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
|
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
|
||||||
@@ -197,9 +200,10 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
toolCalls={itemForDisplay.tools}
|
toolCalls={itemForDisplay.tools}
|
||||||
availableTerminalHeight={availableTerminalHeight}
|
availableTerminalHeight={availableTerminalHeight}
|
||||||
terminalWidth={terminalWidth}
|
terminalWidth={terminalWidth}
|
||||||
borderTop={itemForDisplay.borderTop}
|
borderTop={isFullscreen ? true : itemForDisplay.borderTop}
|
||||||
borderBottom={itemForDisplay.borderBottom}
|
borderBottom={isFullscreen ? true : itemForDisplay.borderBottom}
|
||||||
isExpandable={isExpandable}
|
isExpandable={isExpandable}
|
||||||
|
isFullscreen={isFullscreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{itemForDisplay.type === 'compression' && (
|
{itemForDisplay.type === 'compression' && (
|
||||||
|
|||||||
@@ -49,9 +49,14 @@ export const MainContent = () => {
|
|||||||
mainAreaWidth,
|
mainAreaWidth,
|
||||||
staticAreaMaxItemHeight,
|
staticAreaMaxItemHeight,
|
||||||
cleanUiDetailsVisible,
|
cleanUiDetailsVisible,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
|
terminalHeight,
|
||||||
|
activePtyId,
|
||||||
} = uiState;
|
} = uiState;
|
||||||
const showHeaderDetails = cleanUiDetailsVisible;
|
const showHeaderDetails = cleanUiDetailsVisible;
|
||||||
|
|
||||||
|
const fullscreenHeight = Math.max(terminalHeight - FOOTER_RESERVED_HEIGHT, 5);
|
||||||
|
|
||||||
const lastUserPromptIndex = useMemo(() => {
|
const lastUserPromptIndex = useMemo(() => {
|
||||||
for (let i = uiState.history.length - 1; i >= 0; i--) {
|
for (let i = uiState.history.length - 1; i >= 0; i--) {
|
||||||
const type = uiState.history[i].type;
|
const type = uiState.history[i].type;
|
||||||
@@ -90,9 +95,11 @@ export const MainContent = () => {
|
|||||||
<MemoizedHistoryItemDisplay
|
<MemoizedHistoryItemDisplay
|
||||||
terminalWidth={mainAreaWidth}
|
terminalWidth={mainAreaWidth}
|
||||||
availableTerminalHeight={
|
availableTerminalHeight={
|
||||||
uiState.constrainHeight || !isExpandable
|
isForegroundShellFullscreen
|
||||||
? staticAreaMaxItemHeight
|
? fullscreenHeight
|
||||||
: undefined
|
: uiState.constrainHeight || !isExpandable
|
||||||
|
? staticAreaMaxItemHeight
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -102,6 +109,7 @@ export const MainContent = () => {
|
|||||||
isExpandable={isExpandable}
|
isExpandable={isExpandable}
|
||||||
isFirstThinking={isFirstThinking}
|
isFirstThinking={isFirstThinking}
|
||||||
isFirstAfterThinking={isFirstAfterThinking}
|
isFirstAfterThinking={isFirstAfterThinking}
|
||||||
|
isFullscreen={isForegroundShellFullscreen}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -111,6 +119,8 @@ export const MainContent = () => {
|
|||||||
staticAreaMaxItemHeight,
|
staticAreaMaxItemHeight,
|
||||||
uiState.slashCommands,
|
uiState.slashCommands,
|
||||||
uiState.constrainHeight,
|
uiState.constrainHeight,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
|
fullscreenHeight,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -141,7 +151,11 @@ export const MainContent = () => {
|
|||||||
<HistoryItemDisplay
|
<HistoryItemDisplay
|
||||||
key={i}
|
key={i}
|
||||||
availableTerminalHeight={
|
availableTerminalHeight={
|
||||||
uiState.constrainHeight ? staticAreaMaxItemHeight : undefined
|
isForegroundShellFullscreen
|
||||||
|
? fullscreenHeight
|
||||||
|
: uiState.constrainHeight
|
||||||
|
? staticAreaMaxItemHeight
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
terminalWidth={mainAreaWidth}
|
terminalWidth={mainAreaWidth}
|
||||||
item={{ ...item, id: 0 }}
|
item={{ ...item, id: 0 }}
|
||||||
@@ -149,6 +163,7 @@ export const MainContent = () => {
|
|||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
isFirstThinking={isFirstThinking}
|
isFirstThinking={isFirstThinking}
|
||||||
isFirstAfterThinking={isFirstAfterThinking}
|
isFirstAfterThinking={isFirstAfterThinking}
|
||||||
|
isFullscreen={isForegroundShellFullscreen}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -165,11 +180,41 @@ export const MainContent = () => {
|
|||||||
showConfirmationQueue,
|
showConfirmationQueue,
|
||||||
confirmingTool,
|
confirmingTool,
|
||||||
uiState.history,
|
uiState.history,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
|
fullscreenHeight,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const virtualizedData = useMemo(
|
const virtualizedData = useMemo(() => {
|
||||||
() => [
|
if (isForegroundShellFullscreen && activePtyId) {
|
||||||
|
// Find the item that contains the active PTY
|
||||||
|
const historyItem = uiState.history.find(
|
||||||
|
(h) =>
|
||||||
|
h.type === 'tool_group' &&
|
||||||
|
h.tools.some((t) => t.ptyId === activePtyId),
|
||||||
|
);
|
||||||
|
if (historyItem) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'history' as const,
|
||||||
|
item: historyItem,
|
||||||
|
isExpandable: true,
|
||||||
|
isFirstThinking: false,
|
||||||
|
isFirstAfterThinking: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const pendingItem = pendingHistoryItems.find(
|
||||||
|
(h) =>
|
||||||
|
h.type === 'tool_group' &&
|
||||||
|
h.tools.some((t) => t.ptyId === activePtyId),
|
||||||
|
);
|
||||||
|
if (pendingItem) {
|
||||||
|
return [{ type: 'pending' as const }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
{ type: 'header' as const },
|
{ type: 'header' as const },
|
||||||
...augmentedHistory.map(
|
...augmentedHistory.map(
|
||||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ({
|
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ({
|
||||||
@@ -181,9 +226,14 @@ export const MainContent = () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ type: 'pending' as const },
|
{ type: 'pending' as const },
|
||||||
],
|
];
|
||||||
[augmentedHistory],
|
}, [
|
||||||
);
|
augmentedHistory,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
|
activePtyId,
|
||||||
|
uiState.history,
|
||||||
|
pendingHistoryItems,
|
||||||
|
]);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item }: { item: (typeof virtualizedData)[number] }) => {
|
({ item }: { item: (typeof virtualizedData)[number] }) => {
|
||||||
@@ -200,9 +250,11 @@ export const MainContent = () => {
|
|||||||
<MemoizedHistoryItemDisplay
|
<MemoizedHistoryItemDisplay
|
||||||
terminalWidth={mainAreaWidth}
|
terminalWidth={mainAreaWidth}
|
||||||
availableTerminalHeight={
|
availableTerminalHeight={
|
||||||
uiState.constrainHeight || !item.isExpandable
|
isForegroundShellFullscreen
|
||||||
? staticAreaMaxItemHeight
|
? fullscreenHeight
|
||||||
: undefined
|
: uiState.constrainHeight || !item.isExpandable
|
||||||
|
? staticAreaMaxItemHeight
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||||
key={item.item.id}
|
key={item.item.id}
|
||||||
@@ -212,9 +264,34 @@ export const MainContent = () => {
|
|||||||
isExpandable={item.isExpandable}
|
isExpandable={item.isExpandable}
|
||||||
isFirstThinking={item.isFirstThinking}
|
isFirstThinking={item.isFirstThinking}
|
||||||
isFirstAfterThinking={item.isFirstAfterThinking}
|
isFirstAfterThinking={item.isFirstAfterThinking}
|
||||||
|
isFullscreen={isForegroundShellFullscreen}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
if (isForegroundShellFullscreen && activePtyId) {
|
||||||
|
const pendingItem = pendingHistoryItems.find(
|
||||||
|
(h) =>
|
||||||
|
h.type === 'tool_group' &&
|
||||||
|
h.tools.some((t) => t.ptyId === activePtyId),
|
||||||
|
);
|
||||||
|
if (pendingItem) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<HistoryItemDisplay
|
||||||
|
key={0}
|
||||||
|
availableTerminalHeight={fullscreenHeight}
|
||||||
|
terminalWidth={mainAreaWidth}
|
||||||
|
item={{ ...pendingItem, id: 0 }}
|
||||||
|
isPending={true}
|
||||||
|
isExpandable={true}
|
||||||
|
isFirstThinking={false}
|
||||||
|
isFirstAfterThinking={false}
|
||||||
|
isFullscreen={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return pendingItems;
|
return pendingItems;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -226,9 +303,55 @@ export const MainContent = () => {
|
|||||||
pendingItems,
|
pendingItems,
|
||||||
uiState.constrainHeight,
|
uiState.constrainHeight,
|
||||||
staticAreaMaxItemHeight,
|
staticAreaMaxItemHeight,
|
||||||
|
isForegroundShellFullscreen,
|
||||||
|
fullscreenHeight,
|
||||||
|
activePtyId,
|
||||||
|
pendingHistoryItems,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isForegroundShellFullscreen && activePtyId) {
|
||||||
|
const historyItem = uiState.history.find(
|
||||||
|
(h) =>
|
||||||
|
h.type === 'tool_group' && h.tools.some((t) => t.ptyId === activePtyId),
|
||||||
|
);
|
||||||
|
if (historyItem) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" flexGrow={1} display="flex">
|
||||||
|
<HistoryItemDisplay
|
||||||
|
terminalWidth={mainAreaWidth}
|
||||||
|
availableTerminalHeight={fullscreenHeight}
|
||||||
|
key={historyItem.id}
|
||||||
|
item={historyItem}
|
||||||
|
isPending={false}
|
||||||
|
commands={uiState.slashCommands}
|
||||||
|
isExpandable={true}
|
||||||
|
isFullscreen={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const pendingItem = pendingHistoryItems.find(
|
||||||
|
(h) =>
|
||||||
|
h.type === 'tool_group' && h.tools.some((t) => t.ptyId === activePtyId),
|
||||||
|
);
|
||||||
|
if (pendingItem) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" flexGrow={1} display="flex">
|
||||||
|
<HistoryItemDisplay
|
||||||
|
key={0}
|
||||||
|
availableTerminalHeight={fullscreenHeight}
|
||||||
|
terminalWidth={mainAreaWidth}
|
||||||
|
item={{ ...pendingItem, id: 0 }}
|
||||||
|
isPending={true}
|
||||||
|
isExpandable={true}
|
||||||
|
isFullscreen={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isAlternateBuffer) {
|
if (isAlternateBuffer) {
|
||||||
return (
|
return (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
|
|||||||
@@ -38,37 +38,25 @@ import {
|
|||||||
export interface ShellToolMessageProps extends ToolMessageProps {
|
export interface ShellToolMessageProps extends ToolMessageProps {
|
||||||
config?: Config;
|
config?: Config;
|
||||||
isExpandable?: boolean;
|
isExpandable?: boolean;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||||
name,
|
name,
|
||||||
|
|
||||||
description,
|
description,
|
||||||
|
|
||||||
resultDisplay,
|
resultDisplay,
|
||||||
|
|
||||||
status,
|
status,
|
||||||
|
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
|
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
|
|
||||||
emphasis = 'medium',
|
emphasis = 'medium',
|
||||||
|
|
||||||
renderOutputAsMarkdown = true,
|
renderOutputAsMarkdown = true,
|
||||||
|
|
||||||
ptyId,
|
ptyId,
|
||||||
|
|
||||||
config,
|
config,
|
||||||
|
|
||||||
isFirst,
|
isFirst,
|
||||||
|
|
||||||
borderColor,
|
borderColor,
|
||||||
|
|
||||||
borderDimColor,
|
borderDimColor,
|
||||||
|
|
||||||
isExpandable,
|
isExpandable,
|
||||||
|
isFullscreen,
|
||||||
originalRequestName,
|
originalRequestName,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@@ -93,14 +81,18 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
constrainHeight,
|
constrainHeight,
|
||||||
isExpandable,
|
isExpandable,
|
||||||
|
isFullscreen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const availableHeight = calculateToolContentMaxLines({
|
const availableHeight = calculateToolContentMaxLines({
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
isAlternateBuffer,
|
isAlternateBuffer,
|
||||||
maxLinesLimit: maxLines,
|
maxLinesLimit: maxLines,
|
||||||
|
isFullscreen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lastDimensionsRef = React.useRef({ width: 0, height: 0 });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const isExecuting = status === CoreToolCallStatus.Executing;
|
const isExecuting = status === CoreToolCallStatus.Executing;
|
||||||
if (isExecuting && ptyId) {
|
if (isExecuting && ptyId) {
|
||||||
@@ -109,11 +101,20 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
const finalHeight =
|
const finalHeight =
|
||||||
availableHeight ?? ACTIVE_SHELL_MAX_LINES - SHELL_CONTENT_OVERHEAD;
|
availableHeight ?? ACTIVE_SHELL_MAX_LINES - SHELL_CONTENT_OVERHEAD;
|
||||||
|
|
||||||
ShellExecutionService.resizePty(
|
if (
|
||||||
ptyId,
|
lastDimensionsRef.current.width !== childWidth ||
|
||||||
Math.max(1, childWidth),
|
lastDimensionsRef.current.height !== finalHeight
|
||||||
Math.max(1, finalHeight),
|
) {
|
||||||
);
|
ShellExecutionService.resizePty(
|
||||||
|
ptyId,
|
||||||
|
Math.max(1, childWidth),
|
||||||
|
Math.max(1, finalHeight),
|
||||||
|
);
|
||||||
|
lastDimensionsRef.current = {
|
||||||
|
width: childWidth,
|
||||||
|
height: finalHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
@@ -142,11 +143,8 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
|
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
|
||||||
|
|
||||||
const headerRef = React.useRef<DOMElement>(null);
|
const headerRef = React.useRef<DOMElement>(null);
|
||||||
|
|
||||||
const contentRef = React.useRef<DOMElement>(null);
|
const contentRef = React.useRef<DOMElement>(null);
|
||||||
|
|
||||||
// The shell is focusable if it's the shell command, it's executing, and the interactive shell is enabled.
|
|
||||||
|
|
||||||
const isThisShellFocusable = checkIsShellFocusable(name, status, config);
|
const isThisShellFocusable = checkIsShellFocusable(name, status, config);
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
@@ -156,7 +154,6 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useMouseClick(headerRef, handleFocus, { isActive: !!isThisShellFocusable });
|
useMouseClick(headerRef, handleFocus, { isActive: !!isThisShellFocusable });
|
||||||
|
|
||||||
useMouseClick(contentRef, handleFocus, { isActive: !!isThisShellFocusable });
|
useMouseClick(contentRef, handleFocus, { isActive: !!isThisShellFocusable });
|
||||||
|
|
||||||
const { shouldShowFocusHint } = useFocusHint(
|
const { shouldShowFocusHint } = useFocusHint(
|
||||||
@@ -169,7 +166,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
<>
|
<>
|
||||||
<StickyHeader
|
<StickyHeader
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
isFirst={isFirst}
|
isFirst={isFullscreen ? true : isFirst}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
borderDimColor={borderDimColor}
|
borderDimColor={borderDimColor}
|
||||||
containerRef={headerRef}
|
containerRef={headerRef}
|
||||||
@@ -216,6 +213,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||||
hasFocus={isThisShellFocused}
|
hasFocus={isThisShellFocused}
|
||||||
maxLines={maxLines}
|
maxLines={maxLines}
|
||||||
|
isFullscreen={isFullscreen}
|
||||||
/>
|
/>
|
||||||
{isThisShellFocused && config && (
|
{isThisShellFocused && config && (
|
||||||
<ShellInputPrompt
|
<ShellInputPrompt
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface ToolGroupMessageProps {
|
|||||||
borderTop?: boolean;
|
borderTop?: boolean;
|
||||||
borderBottom?: boolean;
|
borderBottom?: boolean;
|
||||||
isExpandable?: boolean;
|
isExpandable?: boolean;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main component renders the border and maps the tools using ToolMessage
|
// Main component renders the border and maps the tools using ToolMessage
|
||||||
@@ -48,6 +49,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
borderTop: borderTopOverride,
|
borderTop: borderTopOverride,
|
||||||
borderBottom: borderBottomOverride,
|
borderBottom: borderBottomOverride,
|
||||||
isExpandable,
|
isExpandable,
|
||||||
|
isFullscreen,
|
||||||
}) => {
|
}) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
|
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
|
||||||
@@ -140,7 +142,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
const horizontalMargin = TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
||||||
|
const contentWidth = terminalWidth - horizontalMargin;
|
||||||
|
|
||||||
// If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity
|
// If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity
|
||||||
// internal errors, plan-mode hidden write/edit), we should not emit standalone
|
// internal errors, plan-mode hidden write/edit), we should not emit standalone
|
||||||
@@ -164,7 +167,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
cause tearing.
|
cause tearing.
|
||||||
*/
|
*/
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
paddingRight={horizontalMargin}
|
||||||
>
|
>
|
||||||
{visibleToolCalls.map((tool, index) => {
|
{visibleToolCalls.map((tool, index) => {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
@@ -182,6 +185,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
borderColor,
|
borderColor,
|
||||||
borderDimColor,
|
borderDimColor,
|
||||||
isExpandable,
|
isExpandable,
|
||||||
|
isFullscreen,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -226,7 +230,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
*/
|
*/
|
||||||
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
|
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
|
||||||
<Box
|
<Box
|
||||||
height={0}
|
height={isFullscreen ? 1 : 0}
|
||||||
width={contentWidth}
|
width={contentWidth}
|
||||||
borderLeft={true}
|
borderLeft={true}
|
||||||
borderRight={true}
|
borderRight={true}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
|||||||
embeddedShellFocused?: boolean;
|
embeddedShellFocused?: boolean;
|
||||||
ptyId?: number;
|
ptyId?: number;
|
||||||
config?: Config;
|
config?: Config;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface ToolResultDisplayProps {
|
|||||||
maxLines?: number;
|
maxLines?: number;
|
||||||
hasFocus?: boolean;
|
hasFocus?: boolean;
|
||||||
overflowDirection?: 'top' | 'bottom';
|
overflowDirection?: 'top' | 'bottom';
|
||||||
|
isFullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileDiffResult {
|
interface FileDiffResult {
|
||||||
@@ -49,6 +50,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|||||||
maxLines,
|
maxLines,
|
||||||
hasFocus = false,
|
hasFocus = false,
|
||||||
overflowDirection = 'top',
|
overflowDirection = 'top',
|
||||||
|
isFullscreen = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { renderMarkdown } = useUIState();
|
const { renderMarkdown } = useUIState();
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
@@ -57,6 +59,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
isAlternateBuffer,
|
isAlternateBuffer,
|
||||||
maxLinesLimit: maxLines,
|
maxLinesLimit: maxLines,
|
||||||
|
isFullscreen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const combinedPaddingAndBorderWidth = 4;
|
const combinedPaddingAndBorderWidth = 4;
|
||||||
@@ -173,11 +176,13 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|||||||
// Virtualized path for large ANSI arrays
|
// Virtualized path for large ANSI arrays
|
||||||
if (Array.isArray(resultDisplay)) {
|
if (Array.isArray(resultDisplay)) {
|
||||||
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
|
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
|
||||||
const listHeight = Math.min(
|
const listHeight = isFullscreen
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
? limit
|
||||||
(resultDisplay as AnsiOutput).length,
|
: Math.min(
|
||||||
limit,
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
);
|
(resultDisplay as AnsiOutput).length,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
|
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ export interface UIState {
|
|||||||
backgroundShells: Map<number, BackgroundShell>;
|
backgroundShells: Map<number, BackgroundShell>;
|
||||||
activeBackgroundShellPid: number | null;
|
activeBackgroundShellPid: number | null;
|
||||||
backgroundShellHeight: number;
|
backgroundShellHeight: number;
|
||||||
|
isBackgroundShellFullscreen: boolean;
|
||||||
|
isForegroundShellFullscreen: boolean;
|
||||||
isBackgroundShellListOpen: boolean;
|
isBackgroundShellListOpen: boolean;
|
||||||
adminSettingsChanged: boolean;
|
adminSettingsChanged: boolean;
|
||||||
newAgents: AgentDefinition[] | null;
|
newAgents: AgentDefinition[] | null;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export function useBackgroundShellManager({
|
|||||||
}: BackgroundShellManagerProps) {
|
}: BackgroundShellManagerProps) {
|
||||||
const [isBackgroundShellListOpen, setIsBackgroundShellListOpen] =
|
const [isBackgroundShellListOpen, setIsBackgroundShellListOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isBackgroundShellFullscreen, setIsBackgroundShellFullscreen] =
|
||||||
|
useState(false);
|
||||||
const [activeBackgroundShellPid, setActiveBackgroundShellPid] = useState<
|
const [activeBackgroundShellPid, setActiveBackgroundShellPid] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
@@ -73,17 +75,27 @@ export function useBackgroundShellManager({
|
|||||||
setEmbeddedShellFocused,
|
setEmbeddedShellFocused,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const backgroundShellHeight = useMemo(
|
const backgroundShellHeight = useMemo(() => {
|
||||||
() =>
|
if (!isBackgroundShellVisible || backgroundShells.size === 0) {
|
||||||
isBackgroundShellVisible && backgroundShells.size > 0
|
return 0;
|
||||||
? Math.max(Math.floor(terminalHeight * 0.3), 5)
|
}
|
||||||
: 0,
|
if (isBackgroundShellFullscreen) {
|
||||||
[isBackgroundShellVisible, backgroundShells.size, terminalHeight],
|
// Leave enough room for the footer/composer (approx 7 lines)
|
||||||
);
|
return Math.max(terminalHeight - 7, 5);
|
||||||
|
}
|
||||||
|
return Math.max(Math.floor(terminalHeight * 0.3), 5);
|
||||||
|
}, [
|
||||||
|
isBackgroundShellVisible,
|
||||||
|
backgroundShells.size,
|
||||||
|
terminalHeight,
|
||||||
|
isBackgroundShellFullscreen,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isBackgroundShellListOpen,
|
isBackgroundShellListOpen,
|
||||||
setIsBackgroundShellListOpen,
|
setIsBackgroundShellListOpen,
|
||||||
|
isBackgroundShellFullscreen,
|
||||||
|
setIsBackgroundShellFullscreen,
|
||||||
activeBackgroundShellPid,
|
activeBackgroundShellPid,
|
||||||
setActiveBackgroundShellPid,
|
setActiveBackgroundShellPid,
|
||||||
backgroundShellHeight,
|
backgroundShellHeight,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export enum Command {
|
|||||||
BACKGROUND_SHELL_SELECT = 'background.select',
|
BACKGROUND_SHELL_SELECT = 'background.select',
|
||||||
TOGGLE_BACKGROUND_SHELL = 'background.toggle',
|
TOGGLE_BACKGROUND_SHELL = 'background.toggle',
|
||||||
TOGGLE_BACKGROUND_SHELL_LIST = 'background.toggleList',
|
TOGGLE_BACKGROUND_SHELL_LIST = 'background.toggleList',
|
||||||
|
TOGGLE_SHELL_FULLSCREEN = 'shell.toggleFullscreen',
|
||||||
KILL_BACKGROUND_SHELL = 'background.kill',
|
KILL_BACKGROUND_SHELL = 'background.kill',
|
||||||
UNFOCUS_BACKGROUND_SHELL = 'background.unfocus',
|
UNFOCUS_BACKGROUND_SHELL = 'background.unfocus',
|
||||||
UNFOCUS_BACKGROUND_SHELL_LIST = 'background.unfocusList',
|
UNFOCUS_BACKGROUND_SHELL_LIST = 'background.unfocusList',
|
||||||
@@ -395,6 +396,7 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
|
|||||||
[Command.BACKGROUND_SHELL_SELECT, [new KeyBinding('enter')]],
|
[Command.BACKGROUND_SHELL_SELECT, [new KeyBinding('enter')]],
|
||||||
[Command.TOGGLE_BACKGROUND_SHELL, [new KeyBinding('ctrl+b')]],
|
[Command.TOGGLE_BACKGROUND_SHELL, [new KeyBinding('ctrl+b')]],
|
||||||
[Command.TOGGLE_BACKGROUND_SHELL_LIST, [new KeyBinding('ctrl+l')]],
|
[Command.TOGGLE_BACKGROUND_SHELL_LIST, [new KeyBinding('ctrl+l')]],
|
||||||
|
[Command.TOGGLE_SHELL_FULLSCREEN, [new KeyBinding('ctrl+7')]],
|
||||||
[Command.KILL_BACKGROUND_SHELL, [new KeyBinding('ctrl+k')]],
|
[Command.KILL_BACKGROUND_SHELL, [new KeyBinding('ctrl+k')]],
|
||||||
[Command.UNFOCUS_BACKGROUND_SHELL, [new KeyBinding('shift+tab')]],
|
[Command.UNFOCUS_BACKGROUND_SHELL, [new KeyBinding('shift+tab')]],
|
||||||
[Command.UNFOCUS_BACKGROUND_SHELL_LIST, [new KeyBinding('tab')]],
|
[Command.UNFOCUS_BACKGROUND_SHELL_LIST, [new KeyBinding('tab')]],
|
||||||
@@ -519,6 +521,7 @@ export const commandCategories: readonly CommandCategory[] = [
|
|||||||
Command.BACKGROUND_SHELL_SELECT,
|
Command.BACKGROUND_SHELL_SELECT,
|
||||||
Command.TOGGLE_BACKGROUND_SHELL,
|
Command.TOGGLE_BACKGROUND_SHELL,
|
||||||
Command.TOGGLE_BACKGROUND_SHELL_LIST,
|
Command.TOGGLE_BACKGROUND_SHELL_LIST,
|
||||||
|
Command.TOGGLE_SHELL_FULLSCREEN,
|
||||||
Command.KILL_BACKGROUND_SHELL,
|
Command.KILL_BACKGROUND_SHELL,
|
||||||
Command.UNFOCUS_BACKGROUND_SHELL,
|
Command.UNFOCUS_BACKGROUND_SHELL,
|
||||||
Command.UNFOCUS_BACKGROUND_SHELL_LIST,
|
Command.UNFOCUS_BACKGROUND_SHELL_LIST,
|
||||||
@@ -625,6 +628,8 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
|||||||
[Command.TOGGLE_BACKGROUND_SHELL]:
|
[Command.TOGGLE_BACKGROUND_SHELL]:
|
||||||
'Toggle current background shell visibility.',
|
'Toggle current background shell visibility.',
|
||||||
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: 'Toggle background shell list.',
|
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: 'Toggle background shell list.',
|
||||||
|
[Command.TOGGLE_SHELL_FULLSCREEN]:
|
||||||
|
'Toggle fullscreen mode for the active integrated or background shell.',
|
||||||
[Command.KILL_BACKGROUND_SHELL]: 'Kill the active background shell.',
|
[Command.KILL_BACKGROUND_SHELL]: 'Kill the active background shell.',
|
||||||
[Command.UNFOCUS_BACKGROUND_SHELL]:
|
[Command.UNFOCUS_BACKGROUND_SHELL]:
|
||||||
'Move focus from background shell to Gemini.',
|
'Move focus from background shell to Gemini.',
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ export const DefaultAppLayout: React.FC = () => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
ref={uiState.rootUiRef}
|
ref={uiState.rootUiRef}
|
||||||
>
|
>
|
||||||
<MainContent />
|
<Box flexGrow={1} flexDirection="column">
|
||||||
|
<MainContent />
|
||||||
|
</Box>
|
||||||
|
|
||||||
{uiState.isBackgroundShellVisible &&
|
{uiState.isBackgroundShellVisible &&
|
||||||
|
!uiState.isForegroundShellFullscreen &&
|
||||||
uiState.backgroundShells.size > 0 &&
|
uiState.backgroundShells.size > 0 &&
|
||||||
uiState.activeBackgroundShellPid &&
|
uiState.activeBackgroundShellPid &&
|
||||||
uiState.backgroundShellHeight > 0 &&
|
uiState.backgroundShellHeight > 0 &&
|
||||||
|
|||||||
@@ -39,8 +39,14 @@ export function calculateToolContentMaxLines(options: {
|
|||||||
availableTerminalHeight: number | undefined;
|
availableTerminalHeight: number | undefined;
|
||||||
isAlternateBuffer: boolean;
|
isAlternateBuffer: boolean;
|
||||||
maxLinesLimit?: number;
|
maxLinesLimit?: number;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}): number | undefined {
|
}): number | undefined {
|
||||||
const { availableTerminalHeight, isAlternateBuffer, maxLinesLimit } = options;
|
const {
|
||||||
|
availableTerminalHeight,
|
||||||
|
isAlternateBuffer,
|
||||||
|
maxLinesLimit,
|
||||||
|
isFullscreen,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const reservedLines = isAlternateBuffer
|
const reservedLines = isAlternateBuffer
|
||||||
? TOOL_RESULT_ASB_RESERVED_LINE_COUNT
|
? TOOL_RESULT_ASB_RESERVED_LINE_COUNT
|
||||||
@@ -48,7 +54,8 @@ export function calculateToolContentMaxLines(options: {
|
|||||||
|
|
||||||
let contentHeight = availableTerminalHeight
|
let contentHeight = availableTerminalHeight
|
||||||
? Math.max(
|
? Math.max(
|
||||||
availableTerminalHeight - TOOL_RESULT_STATIC_HEIGHT - reservedLines,
|
availableTerminalHeight -
|
||||||
|
(isFullscreen ? 3 : TOOL_RESULT_STATIC_HEIGHT + reservedLines),
|
||||||
TOOL_RESULT_MIN_LINES_SHOWN + 1,
|
TOOL_RESULT_MIN_LINES_SHOWN + 1,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -78,6 +85,7 @@ export function calculateShellMaxLines(options: {
|
|||||||
availableTerminalHeight: number | undefined;
|
availableTerminalHeight: number | undefined;
|
||||||
constrainHeight: boolean;
|
constrainHeight: boolean;
|
||||||
isExpandable: boolean | undefined;
|
isExpandable: boolean | undefined;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}): number | undefined {
|
}): number | undefined {
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
@@ -86,6 +94,7 @@ export function calculateShellMaxLines(options: {
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
constrainHeight,
|
constrainHeight,
|
||||||
isExpandable,
|
isExpandable,
|
||||||
|
isFullscreen,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// 1. If the user explicitly requested expansion (unconstrained), remove all caps.
|
// 1. If the user explicitly requested expansion (unconstrained), remove all caps.
|
||||||
@@ -102,7 +111,12 @@ export function calculateShellMaxLines(options: {
|
|||||||
|
|
||||||
const maxLinesBasedOnHeight = Math.max(1, availableTerminalHeight - 2);
|
const maxLinesBasedOnHeight = Math.max(1, availableTerminalHeight - 2);
|
||||||
|
|
||||||
// 3. Handle ASB mode focus expansion.
|
// 3. Handle Fullscreen or ASB mode focus expansion.
|
||||||
|
// Fullscreen mode always takes the full available height.
|
||||||
|
if (isFullscreen && isThisShellFocused) {
|
||||||
|
return maxLinesBasedOnHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// We allow a focused shell in ASB mode to take up the full available height,
|
// We allow a focused shell in ASB mode to take up the full available height,
|
||||||
// BUT only if we aren't trying to maintain a constrained view (e.g., history items).
|
// BUT only if we aren't trying to maintain a constrained view (e.g., history items).
|
||||||
if (isAlternateBuffer && isThisShellFocused && !constrainHeight) {
|
if (isAlternateBuffer && isThisShellFocused && !constrainHeight) {
|
||||||
|
|||||||
Reference in New Issue
Block a user