Fix desktop settings modal behavior

This commit is contained in:
Simos Mikelatos
2026-06-18 06:15:17 +00:00
parent 1dbf545fd9
commit 7786763dd1
7 changed files with 157 additions and 131 deletions

View File

@@ -69,7 +69,7 @@ jobs:
cat release/SHASUMS256.txt
- name: Upload branch build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: ${{ steps.artifact.outputs.name }}
path: |

View File

@@ -53,7 +53,7 @@ jobs:
cat release/SHASUMS256.txt
- name: Upload branch build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: ${{ steps.artifact.outputs.name }}
path: |

View File

@@ -60,6 +60,7 @@ export class DesktopWindowManager {
this.tabs = tabs;
this.mainWindow = null;
this.settingsWindow = null;
this.tray = null;
this.launcherLoaded = false;
this.activeContentView = null;
@@ -205,8 +206,13 @@ export class DesktopWindowManager {
}
emitDesktopState() {
if (!this.mainWindow || this.mainWindow.webContents.isDestroyed()) return;
this.mainWindow.webContents.send('cloudcli-desktop:state-updated', this.getDesktopState());
const state = this.getDesktopState();
if (this.mainWindow && !this.mainWindow.webContents.isDestroyed()) {
this.mainWindow.webContents.send('cloudcli-desktop:state-updated', state);
}
if (this.settingsWindow && !this.settingsWindow.webContents.isDestroyed()) {
this.settingsWindow.webContents.send('cloudcli-desktop:state-updated', state);
}
}
emitLauncherCommand(command) {
@@ -214,6 +220,64 @@ export class DesktopWindowManager {
this.mainWindow.webContents.send('cloudcli-desktop:launcher-command', command);
}
emitSettingsCommand(command) {
if (!this.settingsWindow || this.settingsWindow.webContents.isDestroyed()) return;
this.settingsWindow.webContents.send('cloudcli-desktop:launcher-command', command);
}
syncSettingsWindowBounds() {
if (!this.mainWindow || !this.settingsWindow || this.settingsWindow.isDestroyed()) return;
this.settingsWindow.setBounds(this.mainWindow.getBounds());
}
async ensureSettingsWindow(sheet = 'desktop-settings') {
if (!this.mainWindow) return null;
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.syncSettingsWindowBounds();
this.emitSettingsCommand({ type: 'open-sheet', sheet });
this.settingsWindow.focus();
return this.settingsWindow;
}
this.settingsWindow = new BrowserWindow({
parent: this.mainWindow,
modal: true,
show: false,
frame: false,
transparent: true,
hasShadow: false,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
movable: false,
skipTaskbar: true,
backgroundColor: '#00000000',
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: this.getPreloadPath(),
},
});
this.syncSettingsWindowBounds();
this.configureChildWebContents(this.settingsWindow.webContents);
this.settingsWindow.once('ready-to-show', () => this.settingsWindow?.show());
this.settingsWindow.on('closed', () => {
this.settingsWindow = null;
});
await this.settingsWindow.loadFile(this.getLauncherPath(), {
query: { modal: '1', sheet },
});
return this.settingsWindow;
}
closeSettingsWindow() {
if (!this.settingsWindow || this.settingsWindow.isDestroyed()) return;
this.settingsWindow.close();
}
async showTarget(target, { trackTab = true } = {}) {
if (!this.mainWindow) return;
if (trackTab) {
@@ -372,8 +436,8 @@ export class DesktopWindowManager {
label: 'Services',
submenu: [
{
label: 'Computer Access',
click: () => void this.actions.showComputerAccess(),
label: 'Computer Use',
click: () => void this.showDesktopSettings(),
},
],
},
@@ -572,8 +636,13 @@ export class DesktopWindowManager {
async showDesktopSettings() {
if (!this.mainWindow) return this.getDesktopState();
await this.showLauncher();
this.emitLauncherCommand({ type: 'open-sheet', sheet: 'app-settings' });
await this.ensureSettingsWindow('desktop-settings');
return this.getDesktopState();
}
async showLocalSettings() {
if (!this.mainWindow) return this.getDesktopState();
await this.ensureSettingsWindow('local-settings');
return this.getDesktopState();
}
@@ -666,11 +735,17 @@ export class DesktopWindowManager {
if (this.activeContentView) {
this.activeContentView.setBounds(this.getContentViewBounds());
}
this.syncSettingsWindowBounds();
});
this.mainWindow.on('move', () => {
this.syncSettingsWindowBounds();
});
this.mainWindow.on('closed', () => {
this.tabViews.clear();
this.activeContentView = null;
this.settingsWindow = null;
this.mainWindow = null;
this.launcherLoaded = false;
});

View File

@@ -8,6 +8,11 @@ body {
height: 100%;
}
html.cc-modal-window,
body.cc-modal-window {
background: transparent;
}
:root {
--bg: #111315;
--s1: #171a1d;

View File

@@ -21,6 +21,7 @@ window.__MOCK_STATE__ = {
var MOCK = window.__MOCK_STATE__ || {};
var VERSION = window.__APP_VERSION__ || '';
var LOGO_URL = new URL('../../public/logo-32.png', window.location.href).toString();
var SEARCH = new URLSearchParams(window.location.search || '');
function clone(value) {
return JSON.parse(JSON.stringify(value));
@@ -48,7 +49,9 @@ window.__MOCK_STATE__ = {
showComputerAccess: function () { return Promise.resolve(clone(mockState)); },
showEnvironmentPicker: function () { return Promise.resolve(clone(mockState)); },
showLauncher: function () { return Promise.resolve(clone(mockState)); },
showLocalSettings: function () { return Promise.resolve(clone(mockState)); },
showDesktopSettings: function () { return Promise.resolve(clone(mockState)); },
closeSettingsWindow: function () { return Promise.resolve(clone(mockState)); },
showActiveEnvironmentActionsMenu: function () { return Promise.resolve(clone(mockState)); },
openCloudDashboard: function () { return Promise.resolve(clone(mockState)); },
runActiveEnvironmentAction: function () { return Promise.resolve(clone(mockState)); },
@@ -70,9 +73,6 @@ window.__MOCK_STATE__ = {
mockState.computerUse.running = mockState.computerUse.enabled;
return Promise.resolve(clone(mockState));
},
showComputerAccessPermissions: function () {
return Promise.resolve(clone(mockState));
},
openEnvironment: function (id) {
var env = (mockState.environments || []).filter(function (item) { return item.id === id; })[0];
if (env) {
@@ -171,12 +171,12 @@ window.__MOCK_STATE__ = {
function computerUseStatus(state) {
var computerUse = state && state.computerUse ? state.computerUse : {};
if (!computerUse.enabled) {
return { label: 'Off', tone: 'idle', detail: 'CloudCLI cannot use this computer.' };
return { label: 'Disabled', tone: 'idle', detail: 'CloudCLI cannot use this computer.' };
}
if (computerUse.consentMode === 'auto') {
return { label: 'Ready · Unattended', tone: 'warn', detail: 'Trusted agents can use this computer without a local approval prompt.' };
return { label: 'Unattended access', tone: 'warn', detail: 'Trusted agents can use this computer without a local approval prompt.' };
}
return { label: 'Ready · Ask first', tone: 'ok', detail: 'CloudCLI can use this computer. Agents need approval before control starts.' };
return { label: 'Ask before each session', tone: 'ok', detail: 'Agents need approval before control starts.' };
}
var CC = {
@@ -198,6 +198,7 @@ window.__MOCK_STATE__ = {
_reg: {},
_wired: false,
_poll: null,
modalMode: SEARCH.get('modal') === '1',
};
window.CC = CC;
@@ -206,11 +207,14 @@ window.__MOCK_STATE__ = {
var overlay;
CC.setState = function (state) {
var currentSheet = CC.ui.openSheet || (CC.modalMode ? (CC.ui.initialSheet || 'desktop-settings') : null);
var sheetBody = overlay ? overlay.querySelector('.cc-sheet-body') : null;
var scrollTop = sheetBody ? sheetBody.scrollTop : 0;
if (state && typeof state === 'object') CC.state = state;
CC.applyTheme(CC.state);
CC.render(CC.state);
if (CC.ui.openSheet) {
CC.openSheet(CC.ui.openSheet);
if (currentSheet) {
CC.openSheet(currentSheet, { scrollTop: scrollTop });
}
};
@@ -334,11 +338,16 @@ window.__MOCK_STATE__ = {
consentMode: current.consentMode === 'auto' ? 'auto' : 'ask',
});
});
case 'computer-permissions':
return CC.run('Opening system permissions...', function () { return bridge.showComputerAccessPermissions(); });
case 'settings-toggle':
CC.openSheet('app-settings');
return;
return CC.run('Opening desktop settings...', function () { return bridge.showDesktopSettings(); });
case 'desktop-settings-toggle':
return CC.run('Opening desktop settings...', function () { return bridge.showDesktopSettings(); });
case 'local-settings-toggle':
return CC.run('Opening local settings...', function () { return bridge.showLocalSettings(); });
case 'computer-settings-toggle':
return CC.run('Opening desktop settings...', function () { return bridge.showDesktopSettings(); });
case 'settings-close':
return CC.closeSheet();
case 'dashboard':
return CC.run('Opening CloudCLI dashboard...', function () { return bridge.openCloudDashboard(); });
case 'env-action':
@@ -347,15 +356,6 @@ window.__MOCK_STATE__ = {
return CC.run('Opening environment actions...', function () { return bridge.showActiveEnvironmentActionsMenu(); });
case 'env-row-menu':
return CC.run('Opening environment actions...', function () { return bridge.showEnvironmentActionsMenu(node.getAttribute('data-cc-environment-id')); });
case 'local-settings-toggle':
CC.openSheet('local-settings');
return;
case 'computer-settings-toggle':
CC.openSheet('computer-access');
return;
case 'settings-close':
CC.closeSheet();
return;
default:
return;
}
@@ -430,19 +430,26 @@ window.__MOCK_STATE__ = {
'</label>';
};
CC.openSheet = function (sheet) {
if (sheet === 'app-settings') {
CC.renderAppSettings();
} else if (sheet === 'computer-access') {
CC.renderComputerAccess();
CC.openSheet = function (sheet, options) {
options = options || {};
if (sheet === 'desktop-settings') {
CC.renderDesktopSettings();
} else {
CC.renderLocalSettings();
}
CC.ui.openSheet = sheet;
overlay.classList.add('open');
if (typeof options.scrollTop === 'number') {
var body = overlay.querySelector('.cc-sheet-body');
if (body) body.scrollTop = options.scrollTop;
}
};
CC.closeSheet = function () {
if (CC.modalMode && bridge.closeSettingsWindow) {
CC.ui.openSheet = null;
return bridge.closeSettingsWindow();
}
CC.ui.openSheet = null;
overlay.classList.remove('open');
};
@@ -478,46 +485,19 @@ window.__MOCK_STATE__ = {
);
};
CC.buildComputerAccessSections = function (state, options) {
options = options || {};
CC.buildComputerUseSection = function (state) {
var computerUse = state.computerUse || {};
var status = computerUseStatus(state);
var sections = [];
if (options.includeStatus !== false) {
sections.push(CC.renderSection(options.statusEyebrow || 'STATUS', status.label, '' +
'<div class="cc-surface">' +
'<div class="cc-status-badge ' + esc(status.tone) + '">' + esc(status.label) + '</div>' +
'<div class="cc-meta">' + esc(status.detail) + '</div>' +
'</div>'
));
}
sections.push(CC.renderSection(options.accessEyebrow || 'ACCESS', 'Allow desktop access', '' +
var body =
'<div class="cc-surface">' +
'<label class="cc-toggle"><input type="checkbox" data-cc-computer-enabled="true"' + (computerUse.enabled ? ' checked' : '') + '><span><b>Allow desktop access</b><br>Let CloudCLI use the computer. Agents cannot act until you approve a session.</span></label>' +
'</div>'
));
sections.push(CC.renderSection(options.modeEyebrow || 'ACCESS MODE', 'Choose how agent approval works', '' +
'<div class="cc-surface cc-choice-group">' +
CC.renderRadioOption('computer-access-mode', 'ask', computerUse.consentMode !== 'auto', 'Ask before each session', 'Agents can request control, but you approve every session.') +
CC.renderRadioOption('computer-access-mode', 'auto', computerUse.consentMode === 'auto', 'Unattended access', 'Trusted agents can use this computer without a local approval prompt.') +
'</div>'
));
var setupBody = '<div class="cc-surface">' +
'<div class="cc-kv"><span>Linked environments</span><span>' + esc(String(computerUse.connectedCount || 0)) + '</span></div>' +
'<div class="cc-kv"><span>Target environments</span><span>' + esc(String(computerUse.targetCount || 0)) + '</span></div>';
if (options.includeTheme) {
setupBody += '<div class="cc-kv"><span>Theme</span><span>' + esc(themeLabel((state.desktopSettings && state.desktopSettings.themeMode) || 'system')) + '</span></div>';
'<label class="cc-toggle"><input type="checkbox" data-cc-computer-enabled="true"' + (computerUse.enabled ? ' checked' : '') + '><span><b>Enable Computer Use</b><br>Let CloudCLI use the computer. Agents cannot act until you approve a session.</span></label>';
if (computerUse.enabled) {
body += '<div class="cc-choice-group">' +
CC.renderRadioOption('computer-access-mode', 'ask', computerUse.consentMode !== 'auto', 'Ask before each session', 'Agents can request control, but you approve every session.') +
CC.renderRadioOption('computer-access-mode', 'auto', computerUse.consentMode === 'auto', 'Unattended access', 'Trusted agents can use this computer without a local approval prompt.') +
'</div>';
}
if (CC.platform === 'mac') {
setupBody += '<div class="cc-actions-inline"><button class="btn sm" data-cc-action="computer-permissions">Open macOS permissions</button></div>';
}
setupBody += '</div>';
sections.push(CC.renderSection(options.setupEyebrow || 'SETUP', 'System permissions and environment links', setupBody));
return sections;
body += '</div>';
return CC.renderSection('COMPUTER USE', 'Control how agents can use this computer', body);
};
CC.renderLocalSettings = function () {
@@ -530,33 +510,17 @@ window.__MOCK_STATE__ = {
'<label class="cc-toggle"><input type="checkbox" data-cc-setting="exposeLocalServerOnNetwork"' + ((state.desktopSettings || {}).exposeLocalServerOnNetwork ? ' checked' : '') + '><span><b>Allow LAN access</b><br>Use the copied URL from another device on this network.</span></label>' +
'</div>'
),
CC.buildThemeSection(state),
];
CC.renderSheet('Local Settings', 'Manage how Local CloudCLI runs and appears on this computer.', sections);
CC.renderSheet('Local Settings', 'Manage how Local CloudCLI runs on this computer.', sections);
};
CC.renderAppSettings = function () {
CC.renderDesktopSettings = function () {
var state = CC.state || {};
var sections = [
CC.buildLocalServerSection(state, {
eyebrow: 'GENERAL',
title: 'Local CloudCLI',
includePreferences: true,
}),
CC.buildThemeSection(state),
CC.buildComputerUseSection(state),
];
sections.push.apply(sections, CC.buildComputerAccessSections(state, {
statusEyebrow: 'COMPUTER ACCESS',
modeEyebrow: 'APPROVAL MODE',
includeTheme: false,
}));
CC.renderSheet('Settings', 'Manage local behavior, appearance, and desktop access for this computer.', sections);
};
CC.renderComputerAccess = function () {
var state = CC.state || {};
var sections = CC.buildComputerAccessSections(state, { includeTheme: true });
CC.renderSheet('Computer Access', 'Let cloud agents use this computer with explicit approval or unattended access.', sections);
CC.renderSheet('Desktop Settings', 'Manage the desktop app appearance and Computer Use behavior.', sections);
};
CC.render = function (state) {
@@ -564,7 +528,11 @@ window.__MOCK_STATE__ = {
var titlebar = (CC._reg.titlebar || CC.titlebar)(state);
var statusbar = (CC._reg.statusbar || CC.statusbar)(state);
var body = CC._reg.renderBody ? CC._reg.renderBody(state) : '';
app.innerHTML = titlebar + '<div class="cc-body ' + (CC._reg.bodyClass || '') + '">' + body + '</div>' + statusbar;
if (CC.modalMode) {
app.innerHTML = '';
} else {
app.innerHTML = titlebar + '<div class="cc-body ' + (CC._reg.bodyClass || '') + '">' + body + '</div>' + statusbar;
}
if (CC._reg.afterRender) CC._reg.afterRender(state);
};
@@ -651,6 +619,11 @@ window.__MOCK_STATE__ = {
var isWin = /Win/i.test(navigator.platform);
CC.platform = isMac ? 'mac' : (isWin ? 'win' : 'linux');
document.body.classList.add(CC.platform);
CC.ui.initialSheet = SEARCH.get('sheet') || 'desktop-settings';
if (CC.modalMode) {
document.documentElement.classList.add('cc-modal-window');
document.body.classList.add('cc-modal-window');
}
wireEvents();
if (window.matchMedia) {
@@ -664,6 +637,7 @@ window.__MOCK_STATE__ = {
if (bridge.onLauncherCommand) {
bridge.onLauncherCommand(function (command) {
if (command && command.type === 'open-sheet') {
CC.ui.initialSheet = command.sheet || CC.ui.initialSheet || 'desktop-settings';
CC.openSheet(command.sheet);
}
});
@@ -699,8 +673,8 @@ window.__MOCK_STATE__ = {
return '<div class="pane-h"><div><h2 class="pane-title">Local CloudCLI</h2><p class="pane-sub">Run the open-source app on this machine. No account required.</p></div></div>' +
'<div class="card"><div class="card-head"><div><div class="card-t">Local server</div><div class="card-sub mono">' + CC.esc(CC.localUrl(state) || 'Starts on demand') + '</div></div><div class="card-tools"><span class="dot" style="background:' + (state.localServerRunning ? 'var(--ok)' : 'var(--tx3)') + '"></span><button class="icon-btn" data-cc-action="local-settings-toggle" title="Local settings">' + CC.icon('gear', 16) + '</button></div></div>' +
'<div class="card-actions"><button class="btn pri" data-cc-action="local">' + CC.icon('play', 15) + 'Open Local CloudCLI</button><button class="btn" data-cc-action="open-web">' + CC.icon('arrow', 14) + 'Open in browser</button><button class="btn" data-cc-action="copy-web">' + CC.icon('copy', 14) + 'Copy URL</button></div></div>' +
'<div class="card"><div class="card-head"><div><div class="card-t">Computer Access</div><div class="card-sub">' + CC.esc(computerUseStatus(state).detail) + '</div></div><div class="card-tools"><span class="badge ' + CC.esc(computerUseStatus(state).tone) + '">' + CC.esc(computerUseStatus(state).label) + '</span><button class="icon-btn" data-cc-action="computer-settings-toggle" title="Computer access">' + CC.icon('monitor', 16) + '</button></div></div>' +
'<div class="card-actions"><button class="btn" data-cc-action="computer-settings-toggle">' + CC.icon('settings', 14) + 'Manage access</button></div></div>';
'<div class="card"><div class="card-head"><div><div class="card-t">Computer Use</div><div class="card-sub">' + CC.esc(computerUseStatus(state).detail) + '</div></div><div class="card-tools"><span class="badge ' + CC.esc(computerUseStatus(state).tone) + '">' + CC.esc(computerUseStatus(state).label) + '</span><button class="icon-btn" data-cc-action="computer-settings-toggle" title="Computer Use settings">' + CC.icon('monitor', 16) + '</button></div></div>' +
'<div class="card-actions"><button class="btn" data-cc-action="computer-settings-toggle">' + CC.icon('settings', 14) + 'Open settings</button></div></div>';
}
function envRow(environment) {

View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell, systemPreferences } from 'electron';
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from 'electron';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -71,7 +71,7 @@ async function promptComputerUseConsent(sessionId) {
buttons: ['Allow this session', 'Deny'],
defaultId: 0,
cancelId: 1,
title: 'Computer Access request',
title: 'Computer Use request',
message: 'An agent wants to control this computer',
detail: [
'A cloud agent is requesting control of your mouse, keyboard, and screen for this session.',
@@ -248,38 +248,8 @@ async function copyDiagnostics() {
});
}
async function showMacComputerAccessPermissions() {
if (process.platform !== 'darwin') return;
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
const accessibilityTrusted = systemPreferences.isTrustedAccessibilityClient(false);
const detail = [
`Screen Recording: ${screenStatus === 'granted' ? 'granted' : 'not granted'}`,
`Accessibility: ${accessibilityTrusted ? 'granted' : 'not granted'}`,
'',
'Computer Access needs both permissions to capture the screen and control the mouse and keyboard.',
'After granting a permission, fully quit and reopen CloudCLI so the change takes effect.',
].join('\n');
const { response } = await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, {
type: 'info',
buttons: ['Open Screen Recording', 'Open Accessibility', 'Close'],
defaultId: 0,
cancelId: 2,
title: 'Computer Access Permissions',
message: 'Grant macOS permissions for Computer Access',
detail,
});
if (response === 0) {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
} else if (response === 1) {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility');
}
}
async function showComputerAccess() {
await desktopWindow?.showLauncher();
desktopWindow?.emitLauncherCommand({ type: 'open-sheet', sheet: 'computer-access' });
await desktopWindow?.showDesktopSettings();
return getDesktopState();
}
@@ -693,11 +663,12 @@ function registerIpcHandlers() {
return getDesktopState();
});
ipcMain.handle('cloudcli-desktop:update-computer-use', async (_event, settings) => updateComputerUse(settings));
ipcMain.handle('cloudcli-desktop:show-computer-use-permissions', async () => {
await showMacComputerAccessPermissions();
ipcMain.handle('cloudcli-desktop:show-desktop-settings', async () => desktopWindow.showDesktopSettings());
ipcMain.handle('cloudcli-desktop:show-local-settings', async () => desktopWindow.showLocalSettings());
ipcMain.handle('cloudcli-desktop:close-settings-window', async () => {
desktopWindow.closeSettingsWindow();
return getDesktopState();
});
ipcMain.handle('cloudcli-desktop:show-desktop-settings', async () => desktopWindow.showDesktopSettings());
ipcMain.handle('cloudcli-desktop:show-active-environment-actions-menu', async () => desktopWindow.showActiveEnvironmentActionsMenu());
ipcMain.handle('cloudcli-desktop:show-environment-actions-menu', async (_event, environmentId) => desktopWindow.showEnvironmentActionsMenu(environmentId));
ipcMain.handle('cloudcli-desktop:switch-tab', async (_event, tabId) => desktopWindow.switchDesktopTab(tabId));

View File

@@ -15,9 +15,10 @@ if (window.location.protocol === 'file:') {
showEnvironmentPicker: () => ipcRenderer.invoke('cloudcli-desktop:show-environment-picker'),
showLauncher: () => ipcRenderer.invoke('cloudcli-desktop:show-launcher'),
showComputerAccess: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-access'),
showLocalSettings: () => ipcRenderer.invoke('cloudcli-desktop:show-local-settings'),
updateComputerUse: (settings) => ipcRenderer.invoke('cloudcli-desktop:update-computer-use', settings),
showComputerAccessPermissions: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-access-permissions'),
showDesktopSettings: () => ipcRenderer.invoke('cloudcli-desktop:show-desktop-settings'),
closeSettingsWindow: () => ipcRenderer.invoke('cloudcli-desktop:close-settings-window'),
showActiveEnvironmentActionsMenu: () => ipcRenderer.invoke('cloudcli-desktop:show-active-environment-actions-menu'),
showEnvironmentActionsMenu: (environmentId) => ipcRenderer.invoke('cloudcli-desktop:show-environment-actions-menu', environmentId),
switchTab: (tabId) => ipcRenderer.invoke('cloudcli-desktop:switch-tab', tabId),