diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 008e89fdd5..b25c360f46 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -42,7 +42,7 @@ they appear in the UI. | UI Label | Setting | Description | Default | | ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` | -| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in milliseconds to poll the terminal background color. | `60000` | +| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` | | Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` | | Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` | | Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` | diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index c2b68f44af..00cf211e0a 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -176,9 +176,8 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `true` - **`ui.terminalBackgroundPollingInterval`** (number): - - **Description:** Interval in milliseconds to poll the terminal background - color. - - **Default:** `60000` + - **Description:** Interval in seconds to poll the terminal background color. + - **Default:** `60` - **`ui.customThemes`** (object): - **Description:** Custom theme definitions. diff --git a/package-lock.json b/package-lock.json index 7aa5ac0237..60e1601953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2251,7 +2251,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2432,7 +2431,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2466,7 +2464,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2835,7 +2832,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2869,7 +2865,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -2922,7 +2917,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -4128,7 +4122,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4406,7 +4399,6 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5399,7 +5391,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8409,7 +8400,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8950,7 +8940,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -10552,7 +10541,6 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz", "integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -14311,7 +14299,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14322,7 +14309,6 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16559,7 +16545,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16783,8 +16768,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -16792,7 +16776,6 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16965,7 +16948,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17173,7 +17155,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -17287,7 +17268,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17300,7 +17280,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -18005,7 +17984,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18300,7 +18278,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index de5aa28e5d..7e983b0e24 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -366,9 +366,9 @@ const SETTINGS_SCHEMA = { label: 'Terminal Background Polling Interval', category: 'UI', requiresRestart: false, - default: 60000, + default: 60, description: - 'Interval in milliseconds to poll the terminal background color.', + 'Interval in seconds to poll the terminal background color.', showInDialog: true, }, customThemes: { diff --git a/packages/cli/src/ui/hooks/useTerminalTheme.test.tsx b/packages/cli/src/ui/hooks/useTerminalTheme.test.tsx index dd4d94f1b8..9738c0f8d2 100644 --- a/packages/cli/src/ui/hooks/useTerminalTheme.test.tsx +++ b/packages/cli/src/ui/hooks/useTerminalTheme.test.tsx @@ -37,7 +37,7 @@ const mockSettings = { ui: { theme: 'default', // DEFAULT_THEME.name autoThemeSwitching: true, - terminalBackgroundPollingInterval: 100, // Short interval for testing + terminalBackgroundPollingInterval: 1, // 1 second }, }, }; @@ -76,6 +76,9 @@ describe('useTerminalTheme', () => { mockUnsubscribe.mockClear(); mockHandleThemeSelect.mockClear(); mockSetTerminalBackground.mockClear(); + // Reset any settings modifications + mockSettings.merged.ui.autoThemeSwitching = true; + mockSettings.merged.ui.theme = 'default'; }); afterEach(() => { @@ -99,18 +102,82 @@ describe('useTerminalTheme', () => { it('should poll for terminal background', () => { renderHook(() => useTerminalTheme(mockHandleThemeSelect, config)); - // Fast-forward time - vi.advanceTimersByTime(100); + // Fast-forward time (1 second) + vi.advanceTimersByTime(1000); expect(mockWrite).toHaveBeenCalledWith('\x1b]11;?\x1b\\'); }); + it('should stop polling after 3 unacknowledged attempts', () => { + renderHook(() => useTerminalTheme(mockHandleThemeSelect, config)); + + // Attempt 1 + vi.advanceTimersByTime(1000); + expect(mockWrite).toHaveBeenCalledTimes(1); + + // Attempt 2 + vi.advanceTimersByTime(1000); + expect(mockWrite).toHaveBeenCalledTimes(2); + + // Attempt 3 + vi.advanceTimersByTime(1000); + expect(mockWrite).toHaveBeenCalledTimes(3); + + // Attempt 4 (should be blocked) + vi.advanceTimersByTime(1000); + expect(mockWrite).toHaveBeenCalledTimes(3); + }); + + it('should reset failure count after successful response', () => { + renderHook(() => useTerminalTheme(mockHandleThemeSelect, config)); + + const handler = mockSubscribe.mock.calls[0][0]; + + // Fail twice + vi.advanceTimersByTime(1000); // 1 + vi.advanceTimersByTime(1000); // 2 + expect(mockWrite).toHaveBeenCalledTimes(2); + + // Succeed + handler('rgb:ffff/ffff/ffff'); + + // Should continue polling + vi.advanceTimersByTime(1000); // 3 (reset count, so this is new attempt 1) + expect(mockWrite).toHaveBeenCalledTimes(3); + + // Fail 3 times from now + vi.advanceTimersByTime(1000); // 4 (new attempt 2) + vi.advanceTimersByTime(1000); // 5 (new attempt 3) + vi.advanceTimersByTime(1000); // 6 (new attempt 4 - blocked?) + + // Total calls: 3 (before success) + 3 (after success) = 6. + // Wait, attempt 3 was the one after success. + // Let's trace carefully: + // 1. Poll (0->1) + // 2. Poll (1->2) + // (Reset to 0) + // 3. Poll (0->1) + // 4. Poll (1->2) + // 5. Poll (2->3) + // 6. Blocked (3->3) + // Total expecting 5 calls? No, 3 before success? + // Initial: 0. + // T=1000: Write (Polls=1). Calls=1. + // T=2000: Write (Polls=2). Calls=2. + // Handler called. Polls=0. + // T=3000: Write (Polls=1). Calls=3. + // T=4000: Write (Polls=2). Calls=4. + // T=5000: Write (Polls=3). Calls=5. + // T=6000: Blocked. Calls=5. + + expect(mockWrite).toHaveBeenCalledTimes(5); + }); + it('should switch to light theme when background is light', () => { renderHook(() => useTerminalTheme(mockHandleThemeSelect, config)); const handler = mockSubscribe.mock.calls[0][0]; // Simulate light background response (white) - // OSC 11 response format: rgb:rrrr/gggg/bbbb handler('rgb:ffff/ffff/ffff'); expect(mockSetTerminalBackground).toHaveBeenCalledWith('#ffffff'); @@ -146,7 +213,7 @@ describe('useTerminalTheme', () => { renderHook(() => useTerminalTheme(mockHandleThemeSelect, config)); // Poll should not happen - vi.advanceTimersByTime(100); + vi.advanceTimersByTime(1000); expect(mockWrite).not.toHaveBeenCalled(); mockSettings.merged.ui.autoThemeSwitching = true; diff --git a/packages/cli/src/ui/hooks/useTerminalTheme.ts b/packages/cli/src/ui/hooks/useTerminalTheme.ts index b70eb30efd..f7de0a8298 100644 --- a/packages/cli/src/ui/hooks/useTerminalTheme.ts +++ b/packages/cli/src/ui/hooks/useTerminalTheme.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useStdout } from 'ink'; import { getLuminance, @@ -26,24 +26,35 @@ export function useTerminalTheme( const { stdout } = useStdout(); const settings = useSettings(); const { subscribe, unsubscribe } = useTerminalContext(); + const unacknowledgedPolls = useRef(0); useEffect(() => { - const pollInterval = setInterval(() => { - // Check if auto-switching is enabled - if (settings.merged.ui.autoThemeSwitching === false) { - return; - } + // Check if auto-switching is enabled + if (settings.merged.ui.autoThemeSwitching === false) { + return; + } + const pollIntervalId = setInterval(() => { // Only poll if we are using one of the default themes const currentThemeName = settings.merged.ui.theme; if (!themeManager.isDefaultTheme(currentThemeName)) { return; } + // Stop polling if the terminal isn't responding + if (unacknowledgedPolls.current >= 3) { + clearInterval(pollIntervalId); + return; + } + + unacknowledgedPolls.current += 1; stdout.write('\x1b]11;?\x1b\\'); - }, settings.merged.ui.terminalBackgroundPollingInterval); + }, settings.merged.ui.terminalBackgroundPollingInterval * 1000); const handleTerminalBackground = (colorStr: string) => { + // Reset the counter since we got a response + unacknowledgedPolls.current = 0; + // Parse the response "rgb:rrrr/gggg/bbbb" const match = /^rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})$/.exec( @@ -72,7 +83,7 @@ export function useTerminalTheme( subscribe(handleTerminalBackground); return () => { - clearInterval(pollInterval); + clearInterval(pollIntervalId); unsubscribe(handleTerminalBackground); }; }, [ diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index aac803ca9e..74854cb836 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -179,9 +179,9 @@ }, "terminalBackgroundPollingInterval": { "title": "Terminal Background Polling Interval", - "description": "Interval in milliseconds to poll the terminal background color.", - "markdownDescription": "Interval in milliseconds to poll the terminal background color.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `60000`", - "default": 60000, + "description": "Interval in seconds to poll the terminal background color.", + "markdownDescription": "Interval in seconds to poll the terminal background color.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `60`", + "default": 60, "type": "number" }, "customThemes": {