mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
fix(cli): optimize theme polling, update units to seconds, and add backoff
This commit is contained in:
@@ -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` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
// <Success> (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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user