mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-14 20:10:36 -07:00
feat: add offline/hybrid mode with cloud subagent delegation
This commit is contained in:
@@ -3062,6 +3062,46 @@ describe('loadCliConfig gemmaModelRouter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig offline mode', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should enable offline mode by default from schema defaults', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.isOfflineModeEnabled()).toBe(true);
|
||||
expect(config.getOfflineSettings().localModelRouting).toBe(
|
||||
'stub_default_api',
|
||||
);
|
||||
});
|
||||
|
||||
it('should load explicit offline settings from merged settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
offline: {
|
||||
enabled: false,
|
||||
localModelRouting: 'stub_default_api',
|
||||
},
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.isOfflineModeEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig fileFiltering', () => {
|
||||
const originalArgv = process.argv;
|
||||
|
||||
|
||||
@@ -982,6 +982,7 @@ export async function loadCliConfig(
|
||||
plan: settings.general?.plan?.enabled ?? true,
|
||||
tracker: settings.experimental?.taskTracker,
|
||||
directWebFetch: settings.experimental?.directWebFetch,
|
||||
offline: settings.general?.offline,
|
||||
planSettings: settings.general?.plan?.directory
|
||||
? settings.general.plan
|
||||
: (extensionPlanSettings ?? settings.general?.plan),
|
||||
|
||||
@@ -431,6 +431,31 @@ describe('SettingsSchema', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should have offline mode settings in schema', () => {
|
||||
const offline = getSettingsSchema().general.properties.offline;
|
||||
expect(offline).toBeDefined();
|
||||
expect(offline.type).toBe('object');
|
||||
expect(offline.category).toBe('General');
|
||||
expect(offline.default).toEqual({});
|
||||
expect(offline.requiresRestart).toBe(false);
|
||||
expect(offline.showInDialog).toBe(true);
|
||||
|
||||
const enabled = offline.properties.enabled;
|
||||
expect(enabled).toBeDefined();
|
||||
expect(enabled.type).toBe('boolean');
|
||||
expect(enabled.default).toBe(true);
|
||||
expect(enabled.requiresRestart).toBe(false);
|
||||
expect(enabled.showInDialog).toBe(true);
|
||||
|
||||
const localModelRouting = offline.properties.localModelRouting;
|
||||
expect(localModelRouting).toBeDefined();
|
||||
expect(localModelRouting.type).toBe('enum');
|
||||
expect(localModelRouting.default).toBe('stub_default_api');
|
||||
expect(localModelRouting.options?.map((o) => o.value)).toEqual([
|
||||
'stub_default_api',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have hooksConfig.notifications setting in schema', () => {
|
||||
const setting = getSettingsSchema().hooksConfig?.properties.notifications;
|
||||
expect(setting).toBeDefined();
|
||||
|
||||
@@ -325,6 +325,44 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
offline: {
|
||||
type: 'object',
|
||||
label: 'Offline Mode',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description:
|
||||
'Offline mode settings. Routes work locally by default and delegates complex tasks through a cloud subagent with confirmation.',
|
||||
showInDialog: true,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Offline Mode',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Enable offline mode behavior by default (local-first strategy with explicit cloud delegation).',
|
||||
showInDialog: true,
|
||||
},
|
||||
localModelRouting: {
|
||||
type: 'enum',
|
||||
label: 'Offline Local Model Routing',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: 'stub_default_api',
|
||||
description:
|
||||
'Selects the offline local-model routing strategy. The current stub still routes through the default API backend.',
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{
|
||||
value: 'stub_default_api',
|
||||
label: 'Stub (Default API)',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
retryFetchErrors: {
|
||||
type: 'boolean',
|
||||
label: 'Retry Fetch Errors',
|
||||
|
||||
@@ -101,6 +101,9 @@ vi.mock('../ui/commands/memoryCommand.js', () => ({ memoryCommand: {} }));
|
||||
vi.mock('../ui/commands/modelCommand.js', () => ({
|
||||
modelCommand: { name: 'model' },
|
||||
}));
|
||||
vi.mock('../ui/commands/offlineCommand.js', () => ({
|
||||
offlineCommand: { name: 'offline' },
|
||||
}));
|
||||
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
|
||||
vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} }));
|
||||
vi.mock('../ui/commands/resumeCommand.js', () => ({
|
||||
@@ -247,6 +250,9 @@ describe('BuiltinCommandLoader', () => {
|
||||
|
||||
const mcpCmd = commands.find((c) => c.name === 'mcp');
|
||||
expect(mcpCmd).toBeDefined();
|
||||
|
||||
const offlineCmd = commands.find((c) => c.name === 'offline');
|
||||
expect(offlineCmd).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include permissions command when folder trust is enabled', async () => {
|
||||
|
||||
@@ -43,6 +43,7 @@ import { mcpCommand } from '../ui/commands/mcpCommand.js';
|
||||
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||
import { modelCommand } from '../ui/commands/modelCommand.js';
|
||||
import { oncallCommand } from '../ui/commands/oncallCommand.js';
|
||||
import { offlineCommand } from '../ui/commands/offlineCommand.js';
|
||||
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
|
||||
import { planCommand } from '../ui/commands/planCommand.js';
|
||||
import { policiesCommand } from '../ui/commands/policiesCommand.js';
|
||||
@@ -183,6 +184,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
: [mcpCommand]),
|
||||
memoryCommand,
|
||||
modelCommand,
|
||||
offlineCommand,
|
||||
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
|
||||
...(this.config?.isPlanEnabled() ? [planCommand] : []),
|
||||
policiesCommand,
|
||||
|
||||
@@ -433,6 +433,9 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
|
||||
const [currentModel, setCurrentModel] = useState(config.getModel());
|
||||
const [isOfflineMode, setIsOfflineMode] = useState(
|
||||
config.isOfflineModeEnabled(),
|
||||
);
|
||||
|
||||
const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
|
||||
const [quotaStats, setQuotaStats] = useState<QuotaStats | undefined>(() => {
|
||||
@@ -567,6 +570,9 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const handleModelChanged = () => {
|
||||
setCurrentModel(config.getModel());
|
||||
};
|
||||
const handleOfflineModeChanged = (payload: { enabled: boolean }) => {
|
||||
setIsOfflineMode(payload.enabled);
|
||||
};
|
||||
|
||||
const handleQuotaChanged = (payload: {
|
||||
remaining: number | undefined;
|
||||
@@ -581,9 +587,11 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
|
||||
coreEvents.on(CoreEvent.OfflineModeChanged, handleOfflineModeChanged);
|
||||
coreEvents.on(CoreEvent.QuotaChanged, handleQuotaChanged);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
|
||||
coreEvents.off(CoreEvent.OfflineModeChanged, handleOfflineModeChanged);
|
||||
coreEvents.off(CoreEvent.QuotaChanged, handleQuotaChanged);
|
||||
};
|
||||
}, [config]);
|
||||
@@ -2493,6 +2501,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
queueErrorMessage,
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
isOfflineMode,
|
||||
currentModel,
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
@@ -2604,6 +2613,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
queueErrorMessage,
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
isOfflineMode,
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { offlineCommand } from './offlineCommand.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
|
||||
describe('offlineCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockConfig = {
|
||||
isOfflineModeEnabled: vi.fn().mockReturnValue(true),
|
||||
getOfflineSettings: vi.fn().mockReturnValue({
|
||||
enabled: true,
|
||||
localModelRouting: 'stub_default_api',
|
||||
}),
|
||||
setOfflineMode: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
mockContext = {
|
||||
services: {
|
||||
agentContext: {
|
||||
config: mockConfig,
|
||||
},
|
||||
settings: {
|
||||
setValue: vi.fn(),
|
||||
},
|
||||
},
|
||||
} as unknown as CommandContext;
|
||||
});
|
||||
|
||||
it('shows offline mode status', async () => {
|
||||
if (!offlineCommand.action) {
|
||||
throw new Error('offline command must have an action');
|
||||
}
|
||||
const result = await offlineCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
content: expect.stringContaining('Offline mode is enabled'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('enables offline mode with /offline on', async () => {
|
||||
const onCommand = offlineCommand.subCommands?.find((c) => c.name === 'on');
|
||||
if (!onCommand?.action) {
|
||||
throw new Error('/offline on command must have an action');
|
||||
}
|
||||
|
||||
const result = await onCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'general.offline.enabled',
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
mockContext.services.agentContext?.config.setOfflineMode,
|
||||
).toHaveBeenCalledWith(true);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Offline mode enabled.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('disables offline mode with /offline off', async () => {
|
||||
const offCommand = offlineCommand.subCommands?.find(
|
||||
(c) => c.name === 'off',
|
||||
);
|
||||
if (!offCommand?.action) {
|
||||
throw new Error('/offline off command must have an action');
|
||||
}
|
||||
|
||||
const result = await offCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'general.offline.enabled',
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
mockContext.services.agentContext?.config.setOfflineMode,
|
||||
).toHaveBeenCalledWith(false);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Offline mode disabled.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
CommandKind,
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
} from './types.js';
|
||||
|
||||
function getStatusMessage(context: CommandContext): string {
|
||||
const config = context.services.agentContext?.config;
|
||||
if (!config) {
|
||||
return 'Offline mode status is unavailable because config is not loaded.';
|
||||
}
|
||||
|
||||
const status = config.isOfflineModeEnabled() ? 'enabled' : 'disabled';
|
||||
const offlineSettings = config.getOfflineSettings();
|
||||
|
||||
return `Offline mode is ${status}. Local routing: ${offlineSettings.localModelRouting}. Cloud delegation subagent: cloud-subagent (tool: cloud_subagent).`;
|
||||
}
|
||||
|
||||
async function setOfflineMode(
|
||||
context: CommandContext,
|
||||
enabled: boolean,
|
||||
): Promise<string> {
|
||||
const config = context.services.agentContext?.config;
|
||||
if (!config) {
|
||||
return 'Offline mode could not be changed because config is not loaded.';
|
||||
}
|
||||
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
'general.offline.enabled',
|
||||
enabled,
|
||||
);
|
||||
await config.setOfflineMode(enabled);
|
||||
|
||||
const status = enabled ? 'enabled' : 'disabled';
|
||||
return `Offline mode ${status}.`;
|
||||
}
|
||||
|
||||
const statusCommand: SlashCommand = {
|
||||
name: 'status',
|
||||
description: 'Show current offline mode status',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
isSafeConcurrent: true,
|
||||
action: async (context) => ({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: getStatusMessage(context),
|
||||
}),
|
||||
};
|
||||
|
||||
const enableCommand: SlashCommand = {
|
||||
name: 'on',
|
||||
altNames: ['enable'],
|
||||
description: 'Enable offline mode',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
isSafeConcurrent: true,
|
||||
action: async (context) => ({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: await setOfflineMode(context, true),
|
||||
}),
|
||||
};
|
||||
|
||||
const disableCommand: SlashCommand = {
|
||||
name: 'off',
|
||||
altNames: ['disable'],
|
||||
description: 'Disable offline mode',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
isSafeConcurrent: true,
|
||||
action: async (context) => ({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: await setOfflineMode(context, false),
|
||||
}),
|
||||
};
|
||||
|
||||
export const offlineCommand: SlashCommand = {
|
||||
name: 'offline',
|
||||
description: 'Manage offline mode and cloud delegation behavior',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
isSafeConcurrent: true,
|
||||
subCommands: [statusCommand, enableCommand, disableCommand],
|
||||
action: async (context) => ({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: getStatusMessage(context),
|
||||
}),
|
||||
};
|
||||
@@ -34,6 +34,7 @@ describe('<StatusRow />', () => {
|
||||
contextFileNames: [],
|
||||
showApprovalModeIndicator: ApprovalMode.DEFAULT,
|
||||
allowPlanMode: false,
|
||||
isOfflineMode: false,
|
||||
renderMarkdown: true,
|
||||
currentModel: 'gemini-3',
|
||||
};
|
||||
@@ -140,4 +141,38 @@ describe('<StatusRow />', () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Tip: Test Tip');
|
||||
});
|
||||
|
||||
it('renders offline mode indicator in detailed UI', async () => {
|
||||
(useComposerStatus as Mock).mockReturnValue({
|
||||
isInteractiveShellWaiting: false,
|
||||
showLoadingIndicator: false,
|
||||
showTips: false,
|
||||
showWit: false,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
...defaultUiState,
|
||||
isOfflineMode: true,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<StatusRow
|
||||
showUiDetails={true}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
width: 100,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('offline');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -411,6 +411,11 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
<RawMarkdownIndicator />
|
||||
</Box>
|
||||
)}
|
||||
{uiState.isOfflineMode && (
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<Text color={theme.status.success}>● offline</Text>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
showRow2Minimal &&
|
||||
|
||||
@@ -97,6 +97,33 @@ describe('ToolConfirmationMessage', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use allow/always allow/deny labels for cloud-subagent confirmations', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: 'Delegate to cloud-subagent',
|
||||
prompt:
|
||||
'Delegating to cloud-subagent for cloud execution.\nReason: Complex task.\nTask: Analyze migration risks.',
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={confirmationDetails}
|
||||
config={mockConfig}
|
||||
getPreferredEditor={vi.fn()}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
toolName="cloud-subagent"
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('1. Allow');
|
||||
expect(output).toContain('2. Always allow');
|
||||
expect(output).toContain('3. Deny (esc)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display WarningMessage for deceptive URLs in info type', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'info',
|
||||
|
||||
@@ -371,29 +371,44 @@ export const ToolConfirmationMessage: React.FC<
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const isCloudSubagentConfirmation =
|
||||
toolName === 'cloud-subagent' || toolName === 'cloud_subagent';
|
||||
|
||||
options.push({
|
||||
label: 'Allow once',
|
||||
label: isCloudSubagentConfirmation ? 'Allow' : 'Allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Allow once',
|
||||
key: isCloudSubagentConfirmation ? 'Allow' : 'Allow once',
|
||||
});
|
||||
if (isTrustedFolder) {
|
||||
options.push({
|
||||
label: 'Allow for this session',
|
||||
label: isCloudSubagentConfirmation
|
||||
? 'Always allow'
|
||||
: 'Allow for this session',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Allow for this session',
|
||||
key: isCloudSubagentConfirmation
|
||||
? 'Always allow'
|
||||
: 'Allow for this session',
|
||||
});
|
||||
if (allowPermanentApproval) {
|
||||
options.push({
|
||||
label: 'Allow for all future sessions',
|
||||
label: isCloudSubagentConfirmation
|
||||
? 'Always allow for all future sessions'
|
||||
: 'Allow for all future sessions',
|
||||
value: ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
key: 'Allow for all future sessions',
|
||||
key: isCloudSubagentConfirmation
|
||||
? 'Always allow for all future sessions'
|
||||
: 'Allow for all future sessions',
|
||||
});
|
||||
}
|
||||
}
|
||||
options.push({
|
||||
label: 'No, suggest changes (esc)',
|
||||
label: isCloudSubagentConfirmation
|
||||
? 'Deny (esc)'
|
||||
: 'No, suggest changes (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
key: 'No, suggest changes (esc)',
|
||||
key: isCloudSubagentConfirmation
|
||||
? 'Deny (esc)'
|
||||
: 'No, suggest changes (esc)',
|
||||
});
|
||||
} else if (confirmationDetails.type === 'mcp') {
|
||||
options.push({
|
||||
@@ -433,6 +448,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
allowPermanentApproval,
|
||||
config,
|
||||
isDiffingEnabled,
|
||||
toolName,
|
||||
]);
|
||||
|
||||
const availableBodyContentHeight = useCallback(() => {
|
||||
|
||||
@@ -157,6 +157,7 @@ export interface UIState {
|
||||
queueErrorMessage: string | null;
|
||||
showApprovalModeIndicator: ApprovalMode;
|
||||
allowPlanMode: boolean;
|
||||
isOfflineMode?: boolean;
|
||||
currentModel: string;
|
||||
contextFileNames: string[];
|
||||
errorCount: number;
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useComposerStatus = () => {
|
||||
const uiState = useUIState();
|
||||
const quotaState = useQuotaState();
|
||||
const settings = useSettings();
|
||||
const isOfflineMode = Boolean(uiState.isOfflineMode);
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() =>
|
||||
@@ -64,22 +65,40 @@ export const useComposerStatus = () => {
|
||||
|
||||
if (hideMinimalModeHintWhileBusy) return null;
|
||||
|
||||
switch (showApprovalModeIndicator) {
|
||||
case ApprovalMode.YOLO:
|
||||
return { text: 'YOLO', color: theme.status.error };
|
||||
case ApprovalMode.PLAN:
|
||||
return { text: 'plan', color: theme.status.success };
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
return { text: 'auto edit', color: theme.status.warning };
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
return null;
|
||||
const approvalModeIndicator = (() => {
|
||||
switch (showApprovalModeIndicator) {
|
||||
case ApprovalMode.YOLO:
|
||||
return { text: 'YOLO', color: theme.status.error };
|
||||
case ApprovalMode.PLAN:
|
||||
return { text: 'plan', color: theme.status.success };
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
return { text: 'auto edit', color: theme.status.warning };
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (approvalModeIndicator) {
|
||||
return isOfflineMode
|
||||
? {
|
||||
text: `${approvalModeIndicator.text} + offline`,
|
||||
color: approvalModeIndicator.color,
|
||||
}
|
||||
: approvalModeIndicator;
|
||||
}
|
||||
|
||||
if (isOfflineMode) {
|
||||
return { text: 'offline', color: theme.status.success };
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [
|
||||
uiState.cleanUiDetailsVisible,
|
||||
showLoadingIndicator,
|
||||
uiState.activeHooks.length,
|
||||
showApprovalModeIndicator,
|
||||
isOfflineMode,
|
||||
]);
|
||||
|
||||
const showMinimalContext = isContextUsageHigh(
|
||||
|
||||
Reference in New Issue
Block a user