mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-14 03:50:49 -07:00
improvements and hardening
This commit is contained in:
@@ -436,6 +436,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const [isOfflineMode, setIsOfflineMode] = useState(
|
||||
config.isOfflineModeEnabled(),
|
||||
);
|
||||
const [cloudSubagentActive, setCloudSubagentActive] = useState(false);
|
||||
|
||||
const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
|
||||
const [quotaStats, setQuotaStats] = useState<QuotaStats | undefined>(() => {
|
||||
@@ -573,6 +574,11 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const handleOfflineModeChanged = (payload: { enabled: boolean }) => {
|
||||
setIsOfflineMode(payload.enabled);
|
||||
};
|
||||
const handleCloudSubagentExecution = (payload: {
|
||||
state: 'started' | 'ended';
|
||||
}) => {
|
||||
setCloudSubagentActive(payload.state === 'started');
|
||||
};
|
||||
|
||||
const handleQuotaChanged = (payload: {
|
||||
remaining: number | undefined;
|
||||
@@ -588,10 +594,18 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
|
||||
coreEvents.on(CoreEvent.OfflineModeChanged, handleOfflineModeChanged);
|
||||
coreEvents.on(
|
||||
CoreEvent.CloudSubagentExecution,
|
||||
handleCloudSubagentExecution,
|
||||
);
|
||||
coreEvents.on(CoreEvent.QuotaChanged, handleQuotaChanged);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
|
||||
coreEvents.off(CoreEvent.OfflineModeChanged, handleOfflineModeChanged);
|
||||
coreEvents.off(
|
||||
CoreEvent.CloudSubagentExecution,
|
||||
handleCloudSubagentExecution,
|
||||
);
|
||||
coreEvents.off(CoreEvent.QuotaChanged, handleQuotaChanged);
|
||||
};
|
||||
}, [config]);
|
||||
@@ -2502,6 +2516,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
isOfflineMode,
|
||||
cloudSubagentActive,
|
||||
currentModel,
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
@@ -2614,6 +2629,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
isOfflineMode,
|
||||
cloudSubagentActive,
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { usePulsingColor } from '../hooks/usePulsingColor.js';
|
||||
|
||||
interface PulsingDotProps {
|
||||
/** Full-brightness color */
|
||||
color: string;
|
||||
/** Dim color at the trough of the pulse */
|
||||
dimColor: string;
|
||||
/** Duration of one full pulse cycle in ms */
|
||||
cycleDurationMs: number;
|
||||
/** Whether the dot is actively pulsing. When false, renders static at full color. */
|
||||
active: boolean;
|
||||
/** Optional label text rendered after the dot */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const PulsingDot: React.FC<PulsingDotProps> = ({
|
||||
color,
|
||||
dimColor,
|
||||
cycleDurationMs,
|
||||
active,
|
||||
label,
|
||||
}) => {
|
||||
const currentColor = usePulsingColor(
|
||||
color,
|
||||
dimColor,
|
||||
cycleDurationMs,
|
||||
active,
|
||||
);
|
||||
|
||||
return (
|
||||
<Text color={currentColor}>
|
||||
{active ? '◉' : '●'} {label}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -47,6 +47,8 @@ describe('<StatusRow />', () => {
|
||||
showWit: true,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
isOfflineMode: false,
|
||||
cloudSubagentActive: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
@@ -87,6 +89,8 @@ describe('<StatusRow />', () => {
|
||||
showWit: false,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
isOfflineMode: false,
|
||||
cloudSubagentActive: false,
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
@@ -116,6 +120,8 @@ describe('<StatusRow />', () => {
|
||||
showWit: true,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
isOfflineMode: false,
|
||||
cloudSubagentActive: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
@@ -150,6 +156,8 @@ describe('<StatusRow />', () => {
|
||||
showWit: false,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
isOfflineMode: true,
|
||||
cloudSubagentActive: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
@@ -175,4 +183,43 @@ describe('<StatusRow />', () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('offline');
|
||||
});
|
||||
|
||||
it('renders cloud indicator when cloud subagent is active', async () => {
|
||||
(useComposerStatus as Mock).mockReturnValue({
|
||||
isInteractiveShellWaiting: false,
|
||||
showLoadingIndicator: false,
|
||||
showTips: false,
|
||||
showWit: false,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
isOfflineMode: true,
|
||||
cloudSubagentActive: true,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
...defaultUiState,
|
||||
isOfflineMode: true,
|
||||
cloudSubagentActive: true,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<StatusRow
|
||||
showUiDetails={true}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
width: 100,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('offline');
|
||||
expect(output).toContain('cloud');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type ThoughtSummary,
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { type ActiveHook } from '../types.js';
|
||||
import { type ActiveHook, StreamingState } from '../types.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
@@ -25,7 +25,9 @@ import { HorizontalLine } from './shared/HorizontalLine.js';
|
||||
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { PulsingDot } from './PulsingDot.js';
|
||||
import { useComposerStatus } from '../hooks/useComposerStatus.js';
|
||||
import { useStreamingContext } from '../contexts/StreamingContext.js';
|
||||
|
||||
/**
|
||||
* Layout constants to prevent magic numbers.
|
||||
@@ -173,7 +175,14 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
showWit,
|
||||
modeContentObj,
|
||||
showMinimalContext,
|
||||
isOfflineMode,
|
||||
cloudSubagentActive,
|
||||
} = useComposerStatus();
|
||||
const streamingState = useStreamingContext();
|
||||
const isLocalActive =
|
||||
isOfflineMode &&
|
||||
streamingState === StreamingState.Responding &&
|
||||
!cloudSubagentActive;
|
||||
|
||||
const [statusWidth, setStatusWidth] = useState(0);
|
||||
const [tipWidth, setTipWidth] = useState(0);
|
||||
@@ -411,9 +420,28 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
<RawMarkdownIndicator />
|
||||
</Box>
|
||||
)}
|
||||
{uiState.isOfflineMode && (
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<Text color={theme.status.success}>● offline</Text>
|
||||
{isOfflineMode && (
|
||||
<Box
|
||||
marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
>
|
||||
<PulsingDot
|
||||
color={theme.status.success}
|
||||
dimColor={theme.ui.dark}
|
||||
cycleDurationMs={1500}
|
||||
active={isLocalActive}
|
||||
label="offline"
|
||||
/>
|
||||
{cloudSubagentActive && (
|
||||
<PulsingDot
|
||||
color={theme.status.warning}
|
||||
dimColor={theme.ui.dark}
|
||||
cycleDurationMs={800}
|
||||
active={true}
|
||||
label="cloud"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -62,18 +62,25 @@ export const SubagentProgressDisplay: React.FC<
|
||||
let headerText: string | undefined;
|
||||
let headerColor = theme.text.secondary;
|
||||
|
||||
const isCloud =
|
||||
progress.agentName === 'cloud-subagent' ||
|
||||
progress.agentName === 'cloud_subagent';
|
||||
const prefix = isCloud ? '☁ Cloud' : `Subagent ${progress.agentName}`;
|
||||
|
||||
if (progress.state === 'cancelled') {
|
||||
headerText = `Subagent ${progress.agentName} was cancelled.`;
|
||||
headerText = `${prefix} was cancelled.`;
|
||||
headerColor = theme.status.warning;
|
||||
} else if (progress.state === 'error') {
|
||||
headerText = `Subagent ${progress.agentName} failed.`;
|
||||
headerText = `${prefix} failed.`;
|
||||
headerColor = theme.status.error;
|
||||
} else if (progress.state === 'completed') {
|
||||
headerText = `Subagent ${progress.agentName} completed.`;
|
||||
headerText = `${prefix} completed.`;
|
||||
headerColor = theme.status.success;
|
||||
} else {
|
||||
headerText = `Running subagent ${progress.agentName}...`;
|
||||
headerColor = theme.text.primary;
|
||||
headerText = isCloud
|
||||
? `☁ Running cloud subagent...`
|
||||
: `Running subagent ${progress.agentName}...`;
|
||||
headerColor = isCloud ? theme.status.warning : theme.text.primary;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -100,9 +100,9 @@ describe('ToolConfirmationMessage', () => {
|
||||
it('should use allow/always allow/deny labels for cloud-subagent confirmations', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: 'Delegate to cloud-subagent',
|
||||
title: '☁ Delegate to cloud subagent',
|
||||
prompt:
|
||||
'Delegating to cloud-subagent for cloud execution.\nReason: Complex task.\nTask: Analyze migration risks.',
|
||||
'This will run with full tool access in cloud mode.\n\nAnalyze migration risks across the codebase.',
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
|
||||
@@ -372,7 +372,9 @@ export const ToolConfirmationMessage: React.FC<
|
||||
});
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const isCloudSubagentConfirmation =
|
||||
toolName === 'cloud-subagent' || toolName === 'cloud_subagent';
|
||||
toolName === 'cloud-subagent' ||
|
||||
toolName === 'cloud_subagent' ||
|
||||
confirmationDetails.title?.includes('cloud subagent');
|
||||
|
||||
options.push({
|
||||
label: isCloudSubagentConfirmation ? 'Allow' : 'Allow once',
|
||||
|
||||
+13
-113
@@ -8,7 +8,7 @@ exports[`ToolConfirmationMessage > enablePermanentToolApproval setting > should
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. Allow for this file in all future sessions ~/.gemini/policies/auto-saved.toml
|
||||
4. Modify with external editor
|
||||
@@ -16,92 +16,6 @@ Apply this change?
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large edit diffs 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ... 10 hidden (Ctrl+O) ... │
|
||||
│ 6 - const oldLine6 = true; │
|
||||
│ 6 + const newLine6 = true; │
|
||||
│ 7 - const oldLine7 = true; │
|
||||
│ 7 + const newLine7 = true; │
|
||||
│ 8 - const oldLine8 = true; │
|
||||
│ 8 + const newLine8 = true; │
|
||||
│ 9 - const oldLine9 = true; │
|
||||
│ 9 + const newLine9 = true; │
|
||||
│ 10 - const oldLine10 = true; │
|
||||
│ 10 + const newLine10 = true; │
|
||||
│ 11 - const oldLine11 = true; │
|
||||
│ 11 + const newLine11 = true; │
|
||||
│ 12 - const oldLine12 = true; │
|
||||
│ 12 + const newLine12 = true; │
|
||||
│ 13 - const oldLine13 = true; │
|
||||
│ 13 + const newLine13 = true; │
|
||||
│ 14 - const oldLine14 = true; │
|
||||
│ 14 + const newLine14 = true; │
|
||||
│ 15 - const oldLine15 = true; │
|
||||
│ 15 + const newLine15 = true; │
|
||||
│ 16 - const oldLine16 = true; │
|
||||
│ 16 + const newLine16 = true; │
|
||||
│ 17 - const oldLine17 = true; │
|
||||
│ 17 + const newLine17 = true; │
|
||||
│ 18 - const oldLine18 = true; │
|
||||
│ 18 + const newLine18 = true; │
|
||||
│ 19 - const oldLine19 = true; │
|
||||
│ 19 + const newLine19 = true; │
|
||||
│ 20 - const oldLine20 = true; │
|
||||
│ 20 + const newLine20 = true; │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. Modify with external editor
|
||||
4. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large exec commands 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ... 19 hidden (Ctrl+O) ... │
|
||||
│ echo "Line 20" │
|
||||
│ echo "Line 21" │
|
||||
│ echo "Line 22" │
|
||||
│ echo "Line 23" │
|
||||
│ echo "Line 24" │
|
||||
│ echo "Line 25" │
|
||||
│ echo "Line 26" │
|
||||
│ echo "Line 27" │
|
||||
│ echo "Line 28" │
|
||||
│ echo "Line 29" │
|
||||
│ echo "Line 30" │
|
||||
│ echo "Line 31" │
|
||||
│ echo "Line 32" │
|
||||
│ echo "Line 33" │
|
||||
│ echo "Line 34" │
|
||||
│ echo "Line 35" │
|
||||
│ echo "Line 36" │
|
||||
│ echo "Line 37" │
|
||||
│ echo "Line 38" │
|
||||
│ echo "Line 39" │
|
||||
│ echo "Line 40" │
|
||||
│ echo "Line 41" │
|
||||
│ echo "Line 42" │
|
||||
│ echo "Line 43" │
|
||||
│ echo "Line 44" │
|
||||
│ echo "Line 45" │
|
||||
│ echo "Line 46" │
|
||||
│ echo "Line 47" │
|
||||
│ echo "Line 48" │
|
||||
│ echo "Line 49" │
|
||||
│ echo "Line 50" │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Allow execution of [echo]?
|
||||
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > should display multiple commands for exec type when provided 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ echo "hello" │
|
||||
@@ -112,7 +26,7 @@ exports[`ToolConfirmationMessage > should display multiple commands for exec typ
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Allow execution of [echo, ls, whoami]?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -125,7 +39,7 @@ URLs to fetch:
|
||||
- https://raw.githubusercontent.com/google/gemini-react/main/README.md
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -135,32 +49,18 @@ exports[`ToolConfirmationMessage > should not display urls if prompt and url are
|
||||
"https://example.com
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > should render multiline shell scripts with correct newlines and syntax highlighting 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ echo "hello" │
|
||||
│ for i in 1 2 3; do │
|
||||
│ echo $i │
|
||||
│ done │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Allow execution of [echo]?
|
||||
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationMessage > should strip BiDi characters from MCP tool and server names 1`] = `
|
||||
"MCP Server: testserver
|
||||
Tool: testtool
|
||||
Allow execution of MCP tool "testtool" from server "testserver"?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow tool for this session
|
||||
3. Allow all server tools for this session
|
||||
4. No, suggest changes (esc)
|
||||
@@ -175,7 +75,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Modify with external editor
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -189,7 +89,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Apply this change?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. Modify with external editor
|
||||
4. No, suggest changes (esc)
|
||||
@@ -202,7 +102,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Allow execution of [echo]?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -213,7 +113,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Allow execution of [echo]?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -223,7 +123,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
|
||||
"https://example.com
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -232,7 +132,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
|
||||
"https://example.com
|
||||
Do you want to proceed?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow for this session
|
||||
3. No, suggest changes (esc)
|
||||
"
|
||||
@@ -243,7 +143,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
|
||||
Tool: test-tool
|
||||
Allow execution of MCP tool "test-tool" from server "test-server"?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. No, suggest changes (esc)
|
||||
"
|
||||
`;
|
||||
@@ -253,7 +153,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
|
||||
Tool: test-tool
|
||||
Allow execution of MCP tool "test-tool" from server "test-server"?
|
||||
|
||||
● 1. Allow once
|
||||
● 1. Allow once
|
||||
2. Allow tool for this session
|
||||
3. Allow all server tools for this session
|
||||
4. No, suggest changes (esc)
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface UIState {
|
||||
showApprovalModeIndicator: ApprovalMode;
|
||||
allowPlanMode: boolean;
|
||||
isOfflineMode?: boolean;
|
||||
cloudSubagentActive?: boolean;
|
||||
currentModel: string;
|
||||
contextFileNames: string[];
|
||||
errorCount: number;
|
||||
|
||||
@@ -22,6 +22,7 @@ export const useComposerStatus = () => {
|
||||
const quotaState = useQuotaState();
|
||||
const settings = useSettings();
|
||||
const isOfflineMode = Boolean(uiState.isOfflineMode);
|
||||
const cloudSubagentActive = Boolean(uiState.cloudSubagentActive);
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() =>
|
||||
@@ -80,14 +81,23 @@ export const useComposerStatus = () => {
|
||||
})();
|
||||
|
||||
if (approvalModeIndicator) {
|
||||
return isOfflineMode
|
||||
const suffix = cloudSubagentActive
|
||||
? ' + cloud'
|
||||
: isOfflineMode
|
||||
? ' + offline'
|
||||
: '';
|
||||
return suffix
|
||||
? {
|
||||
text: `${approvalModeIndicator.text} + offline`,
|
||||
text: `${approvalModeIndicator.text}${suffix}`,
|
||||
color: approvalModeIndicator.color,
|
||||
}
|
||||
: approvalModeIndicator;
|
||||
}
|
||||
|
||||
if (cloudSubagentActive) {
|
||||
return { text: 'cloud', color: theme.status.warning };
|
||||
}
|
||||
|
||||
if (isOfflineMode) {
|
||||
return { text: 'offline', color: theme.status.success };
|
||||
}
|
||||
@@ -99,6 +109,7 @@ export const useComposerStatus = () => {
|
||||
uiState.activeHooks.length,
|
||||
showApprovalModeIndicator,
|
||||
isOfflineMode,
|
||||
cloudSubagentActive,
|
||||
]);
|
||||
|
||||
const showMinimalContext = isContextUsageHigh(
|
||||
@@ -127,5 +138,7 @@ export const useComposerStatus = () => {
|
||||
showWit,
|
||||
modeContentObj,
|
||||
showMinimalContext,
|
||||
isOfflineMode,
|
||||
cloudSubagentActive,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { interpolateColor } from '../themes/color-utils.js';
|
||||
|
||||
const FRAME_INTERVAL_MS = 60; // ~16fps — smooth enough for a pulse, cheap on CPU
|
||||
|
||||
/**
|
||||
* Returns a color that pulses between `activeColor` and `dimColor` on a sine
|
||||
* curve. When `active` is false the hook stops its timer and returns
|
||||
* `activeColor` at full brightness (static dot).
|
||||
*/
|
||||
export function usePulsingColor(
|
||||
activeColor: string,
|
||||
dimColor: string,
|
||||
cycleDurationMs: number,
|
||||
active: boolean,
|
||||
): string {
|
||||
const [time, setTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
setTime(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTime((prev) => prev + FRAME_INTERVAL_MS);
|
||||
}, FRAME_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [active]);
|
||||
|
||||
if (!active) {
|
||||
return activeColor;
|
||||
}
|
||||
|
||||
// Sine oscillation: 0 → 1 → 0 over one cycle
|
||||
const progress = (Math.sin((2 * Math.PI * time) / cycleDurationMs) + 1) / 2;
|
||||
return interpolateColor(dimColor, activeColor, progress) || activeColor;
|
||||
}
|
||||
Reference in New Issue
Block a user