diff --git a/.gemini/skills/docs-writer/references/style-guide.md b/.gemini/skills/docs-writer/references/style-guide.md
index d3d219378a..1a846ac6e1 100644
--- a/.gemini/skills/docs-writer/references/style-guide.md
+++ b/.gemini/skills/docs-writer/references/style-guide.md
@@ -33,8 +33,8 @@
a natural tone.
- **Simple vocabulary:** Use common words. Define technical terms when
necessary.
-- **Conciseness:** Keep sentences short and focused, but don't omit
- helpful information.
+- **Conciseness:** Keep sentences short and focused, but don't omit helpful
+ information.
- **"Please":** Avoid using the word "please."
## IV. Procedures and steps
diff --git a/docs/cli/commands.md b/docs/cli/commands.md
index ec9b2396fe..a5b2183206 100644
--- a/docs/cli/commands.md
+++ b/docs/cli/commands.md
@@ -288,12 +288,12 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
These shortcuts apply directly to the input prompt for text manipulation.
- **Undo:**
- - **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input
- prompt.
+ - **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
+ in the input prompt.
- **Redo:**
- - **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action
- in the input prompt.
+ - **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
+ last undone action in the input prompt.
## At commands (`@`)
diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md
index aa2d8200fe..5cfa26cf92 100644
--- a/docs/cli/keyboard-shortcuts.md
+++ b/docs/cli/keyboard-shortcuts.md
@@ -30,17 +30,17 @@ available combinations.
#### Editing
-| Action | Keys |
-| ------------------------------------------------ | --------------------------------------------------------- |
-| Delete from the cursor to the end of the line. | `Ctrl + K` |
-| Delete from the cursor to the start of the line. | `Ctrl + U` |
-| Clear all text in the input field. | `Ctrl + C` |
-| Delete the previous word. | `Ctrl + Backspace`
`Alt + Backspace`
`Ctrl + W` |
-| Delete the next word. | `Ctrl + Delete`
`Alt + Delete` |
-| Delete the character to the left. | `Backspace`
`Ctrl + H` |
-| Delete the character to the right. | `Delete`
`Ctrl + D` |
-| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
-| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
+| Action | Keys |
+| ------------------------------------------------ | ---------------------------------------------------------------- |
+| Delete from the cursor to the end of the line. | `Ctrl + K` |
+| Delete from the cursor to the start of the line. | `Ctrl + U` |
+| Clear all text in the input field. | `Ctrl + C` |
+| Delete the previous word. | `Ctrl + Backspace`
`Alt + Backspace`
`Ctrl + W` |
+| Delete the next word. | `Ctrl + Delete`
`Alt + Delete` |
+| Delete the character to the left. | `Backspace`
`Ctrl + H` |
+| Delete the character to the right. | `Delete`
`Ctrl + D` |
+| Undo the most recent text edit. | `Cmd + Z (no Shift)`
`Alt + Z (no Shift)` |
+| Redo the most recent undone text edit. | `Shift + Ctrl + Z`
`Shift + Cmd + Z`
`Shift + Alt + Z` |
#### Scrolling
@@ -110,6 +110,7 @@ available combinations.
| Focus the Gemini input from the shell input. | `Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
+| Suspend the application (not yet implemented). | `Ctrl + Z` |
diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts
index 86b3580536..0d32ae2922 100644
--- a/packages/cli/src/config/keyBindings.ts
+++ b/packages/cli/src/config/keyBindings.ts
@@ -85,6 +85,7 @@ export enum Command {
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
CLEAR_SCREEN = 'app.clearScreen',
RESTART_APP = 'app.restart',
+ SUSPEND_APP = 'app.suspend',
}
/**
@@ -170,8 +171,15 @@ export const defaultKeyBindings: KeyBindingConfig = {
],
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
- [Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
- [Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
+ [Command.UNDO]: [
+ { key: 'z', cmd: true, shift: false },
+ { key: 'z', alt: true, shift: false },
+ ],
+ [Command.REDO]: [
+ { key: 'z', ctrl: true, shift: true },
+ { key: 'z', cmd: true, shift: true },
+ { key: 'z', alt: true, shift: true },
+ ],
// Scrolling
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
@@ -265,6 +273,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
[Command.RESTART_APP]: [{ key: 'r' }],
+ [Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }],
};
interface CommandCategory {
@@ -374,6 +383,7 @@ export const commandCategories: readonly CommandCategory[] = [
Command.UNFOCUS_SHELL_INPUT,
Command.CLEAR_SCREEN,
Command.RESTART_APP,
+ Command.SUSPEND_APP,
],
},
];
@@ -464,4 +474,5 @@ export const commandDescriptions: Readonly> = {
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
[Command.RESTART_APP]: 'Restart the application.',
+ [Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).',
};
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index b580f2fed6..baeace8f65 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -193,6 +193,7 @@ import {
disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
+import { WARNING_PROMPT_DURATION_MS } from './constants.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
@@ -1900,7 +1901,7 @@ describe('AppContainer State Management', () => {
// Advance timer past the reset threshold
act(() => {
- vi.advanceTimersByTime(1001);
+ vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
});
pressKey({ name: 'c', ctrl: true });
@@ -1945,7 +1946,7 @@ describe('AppContainer State Management', () => {
// Advance timer past the reset threshold
act(() => {
- vi.advanceTimersByTime(1001);
+ vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
});
pressKey({ name: 'd', ctrl: true });
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 507837be87..8c4141a66a 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -1443,6 +1443,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
setShowErrorDetails((prev) => !prev);
return true;
+ } else if (keyMatchers[Command.SUSPEND_APP](key)) {
+ handleWarning('Undo has been moved to Cmd + Z or Alt/Opt + Z');
+ return true;
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
setShowFullTodos((prev) => !prev);
return true;
diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts
index 72db069e58..496217fe9e 100644
--- a/packages/cli/src/ui/constants.ts
+++ b/packages/cli/src/ui/constants.ts
@@ -29,7 +29,7 @@ export const TOOL_STATUS = {
// Maximum number of MCP resources to display per server before truncating
export const MAX_MCP_RESOURCES_TO_SHOW = 10;
-export const WARNING_PROMPT_DURATION_MS = 1000;
+export const WARNING_PROMPT_DURATION_MS = 3000;
export const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
export const SHELL_ACTION_REQUIRED_TITLE_DELAY_MS = 30000;
export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000;
diff --git a/packages/cli/src/ui/constants/tips.ts b/packages/cli/src/ui/constants/tips.ts
index 1ff19af199..1bd6f3233d 100644
--- a/packages/cli/src/ui/constants/tips.ts
+++ b/packages/cli/src/ui/constants/tips.ts
@@ -110,8 +110,8 @@ export const INFORMATIVE_TIPS = [
'Delete from the cursor to the end of the line with Ctrl+K…',
'Clear the entire input prompt with a double-press of Esc…',
'Paste from your clipboard with Ctrl+V…',
- 'Undo text edits in the input with Ctrl+Z…',
- 'Redo undone text edits with Ctrl+Shift+Z…',
+ 'Undo text edits in the input with Cmd+Z or Alt+Z…',
+ 'Redo undone text edits with Shift+Cmd+Z or Shift+Alt+Z…',
'Open the current prompt in an external editor with Ctrl+X…',
'In menus, move up/down with k/j or the arrow keys…',
'In menus, select an item by typing its number…',
diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx
index 661d562f83..91c4eb3493 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.tsx
@@ -124,6 +124,8 @@ function charLengthAt(str: string, i: number): number {
return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
}
+// Note: we do not convert alt+z, alt+shift+z, or alt+v here
+// because mac users have alternative hotkeys.
const MAC_ALT_KEY_CHARACTER_MAP: Record = {
'\u222B': 'b', // "∫" back one word
'\u0192': 'f', // "ƒ" forward one word
diff --git a/packages/cli/src/ui/hooks/useHookDisplayState.test.ts b/packages/cli/src/ui/hooks/useHookDisplayState.test.ts
index a6c86401bd..3f087771c8 100644
--- a/packages/cli/src/ui/hooks/useHookDisplayState.test.ts
+++ b/packages/cli/src/ui/hooks/useHookDisplayState.test.ts
@@ -14,6 +14,7 @@ import {
type HookEndPayload,
} from '@google/gemini-cli-core';
import { act } from 'react';
+import { WARNING_PROMPT_DURATION_MS } from '../constants.js';
describe('useHookDisplayState', () => {
beforeEach(() => {
@@ -53,7 +54,7 @@ describe('useHookDisplayState', () => {
});
});
- it('should remove a hook immediately if duration > 1s', () => {
+ it('should remove a hook immediately if duration > minimum duration', () => {
const { result } = renderHook(() => useHookDisplayState());
const startPayload: HookStartPayload = {
@@ -65,9 +66,9 @@ describe('useHookDisplayState', () => {
coreEvents.emitHookStart(startPayload);
});
- // Advance time by 1.1 seconds
+ // Advance time by slightly more than the minimum duration
act(() => {
- vi.advanceTimersByTime(1100);
+ vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 100);
});
const endPayload: HookEndPayload = {
@@ -83,7 +84,7 @@ describe('useHookDisplayState', () => {
expect(result.current).toHaveLength(0);
});
- it('should delay removal if duration < 1s', () => {
+ it('should delay removal if duration < minimum duration', () => {
const { result } = renderHook(() => useHookDisplayState());
const startPayload: HookStartPayload = {
@@ -113,9 +114,9 @@ describe('useHookDisplayState', () => {
// Should still be present
expect(result.current).toHaveLength(1);
- // Advance remaining time (900ms needed, let's go 950ms)
+ // Advance remaining time + buffer
act(() => {
- vi.advanceTimersByTime(950);
+ vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS - 100 + 50);
});
expect(result.current).toHaveLength(0);
@@ -138,7 +139,7 @@ describe('useHookDisplayState', () => {
expect(result.current).toHaveLength(2);
- // End h1 (total time 500ms -> needs 500ms delay)
+ // End h1 (total time 500ms -> needs remaining delay)
act(() => {
coreEvents.emitHookEnd({
hookName: 'h1',
@@ -150,15 +151,24 @@ describe('useHookDisplayState', () => {
// h1 still there
expect(result.current).toHaveLength(2);
- // Advance 600ms. h1 should disappear. h2 has been running for 600ms.
+ // Advance enough for h1 to expire.
+ // h1 ran for 500ms. Needs WARNING_PROMPT_DURATION_MS total.
+ // So advance WARNING_PROMPT_DURATION_MS - 500 + 100.
+ const advanceForH1 = WARNING_PROMPT_DURATION_MS - 500 + 100;
act(() => {
- vi.advanceTimersByTime(600);
+ vi.advanceTimersByTime(advanceForH1);
});
+ // h1 should disappear. h2 has been running for 500 (initial) + advanceForH1.
expect(result.current).toHaveLength(1);
expect(result.current[0].name).toBe('h2');
- // End h2 (total time 600ms -> needs 400ms delay)
+ // End h2.
+ // h2 duration so far: 0 (start) -> 500 (start h2) -> (end h1) -> advanceForH1.
+ // Actually h2 started at t=500. Current time is t=500 + advanceForH1.
+ // Duration = advanceForH1.
+ // advanceForH1 = 3000 - 500 + 100 = 2600.
+ // So h2 has run for 2600ms. Needs 400ms more.
act(() => {
coreEvents.emitHookEnd({
hookName: 'h2',
@@ -169,6 +179,8 @@ describe('useHookDisplayState', () => {
expect(result.current).toHaveLength(1);
+ // Advance remaining needed for h2 + buffer
+ // 3000 - 2600 = 400.
act(() => {
vi.advanceTimersByTime(500);
});
@@ -199,34 +211,42 @@ describe('useHookDisplayState', () => {
expect(result.current[0].name).toBe('same-hook');
expect(result.current[1].name).toBe('same-hook');
- // End Hook 1 at t=600 (Duration 600ms -> delay 400ms)
+ // End Hook 1 at t=600 (Duration 600ms -> delay needed)
act(() => {
vi.advanceTimersByTime(100);
coreEvents.emitHookEnd({ ...hook, success: true });
});
- // Both still visible (Hook 1 pending removal in 400ms)
+ // Both still visible
expect(result.current).toHaveLength(2);
- // Advance 400ms (t=1000). Hook 1 should be removed.
+ // Advance to make Hook 1 expire.
+ // Hook 1 duration 600ms. Needs WARNING_PROMPT_DURATION_MS total.
+ // Needs WARNING_PROMPT_DURATION_MS - 600 more.
+ const advanceForHook1 = WARNING_PROMPT_DURATION_MS - 600;
act(() => {
- vi.advanceTimersByTime(400);
+ vi.advanceTimersByTime(advanceForHook1);
});
expect(result.current).toHaveLength(1);
- // End Hook 2 at t=1100 (Duration: 1100 - 500 = 600ms -> delay 400ms)
+ // End Hook 2.
+ // Hook 2 started at t=500.
+ // Current time: t = 600 (hook 1 end) + advanceForHook1 = 600 + 3000 - 600 = 3000.
+ // Hook 2 duration = 3000 - 500 = 2500ms.
+ // Needs 3000 - 2500 = 500ms more.
act(() => {
- vi.advanceTimersByTime(100);
+ vi.advanceTimersByTime(100); // just a small step before ending
coreEvents.emitHookEnd({ ...hook, success: true });
});
- // Hook 2 still visible (pending removal in 400ms)
+ // Hook 2 still visible (pending removal)
+ // Total run time: 2500 + 100 = 2600ms. Needs 400ms.
expect(result.current).toHaveLength(1);
- // Advance 400ms (t=1500). Hook 2 should be removed.
+ // Advance remaining
act(() => {
- vi.advanceTimersByTime(400);
+ vi.advanceTimersByTime(500);
});
expect(result.current).toHaveLength(0);
diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts
index c330bf5d14..cf520b84e8 100644
--- a/packages/cli/src/ui/keyMatchers.test.ts
+++ b/packages/cli/src/ui/keyMatchers.test.ts
@@ -131,13 +131,24 @@ describe('keyMatchers', () => {
},
{
command: Command.UNDO,
- positive: [createKey('z', { shift: false, ctrl: true })],
- negative: [createKey('z'), createKey('z', { shift: true, ctrl: true })],
+ positive: [
+ createKey('z', { shift: false, cmd: true }),
+ createKey('z', { shift: false, alt: true }),
+ ],
+ negative: [
+ createKey('z'),
+ createKey('z', { shift: true, cmd: true }),
+ createKey('z', { shift: false, ctrl: true }),
+ ],
},
{
command: Command.REDO,
- positive: [createKey('z', { shift: true, ctrl: true })],
- negative: [createKey('z'), createKey('z', { shift: false, ctrl: true })],
+ positive: [
+ createKey('z', { shift: true, cmd: true }),
+ createKey('z', { shift: true, alt: true }),
+ createKey('z', { shift: true, ctrl: true }),
+ ],
+ negative: [createKey('z'), createKey('z', { shift: false, cmd: true })],
},
// Screen control