diff --git a/.github/workflows/desktop-macos-branch-build.yml b/.github/workflows/desktop-macos-branch-build.yml index d1bc98fc..0038b1d1 100644 --- a/.github/workflows/desktop-macos-branch-build.yml +++ b/.github/workflows/desktop-macos-branch-build.yml @@ -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: | diff --git a/.github/workflows/desktop-windows-branch-build.yml b/.github/workflows/desktop-windows-branch-build.yml index 0528d7a4..362c1025 100644 --- a/.github/workflows/desktop-windows-branch-build.yml +++ b/.github/workflows/desktop-windows-branch-build.yml @@ -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: | diff --git a/electron/desktopWindow.js b/electron/desktopWindow.js index 606f5f16..1f1143fa 100644 --- a/electron/desktopWindow.js +++ b/electron/desktopWindow.js @@ -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; }); diff --git a/electron/launcher/launcher.css b/electron/launcher/launcher.css index edd18acc..b361cd18 100644 --- a/electron/launcher/launcher.css +++ b/electron/launcher/launcher.css @@ -8,6 +8,11 @@ body { height: 100%; } +html.cc-modal-window, +body.cc-modal-window { + background: transparent; +} + :root { --bg: #111315; --s1: #171a1d; diff --git a/electron/launcher/launcher.js b/electron/launcher/launcher.js index 0d5f2aa3..ca47890b 100644 --- a/electron/launcher/launcher.js +++ b/electron/launcher/launcher.js @@ -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__ = { ''; }; - 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, '' + - '
' + - '
' + esc(status.label) + '
' + - '
' + esc(status.detail) + '
' + - '
' - )); - } - - sections.push(CC.renderSection(options.accessEyebrow || 'ACCESS', 'Allow desktop access', '' + + var body = '
' + - '' + - '
' - )); - - sections.push(CC.renderSection(options.modeEyebrow || 'ACCESS MODE', 'Choose how agent approval works', '' + - '
' + - 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.') + - '
' - )); - - var setupBody = '
' + - '
Linked environments' + esc(String(computerUse.connectedCount || 0)) + '
' + - '
Target environments' + esc(String(computerUse.targetCount || 0)) + '
'; - if (options.includeTheme) { - setupBody += '
Theme' + esc(themeLabel((state.desktopSettings && state.desktopSettings.themeMode) || 'system')) + '
'; + ''; + if (computerUse.enabled) { + body += '
' + + 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.') + + '
'; } - if (CC.platform === 'mac') { - setupBody += '
'; - } - setupBody += '
'; - sections.push(CC.renderSection(options.setupEyebrow || 'SETUP', 'System permissions and environment links', setupBody)); - return sections; + body += ''; + return CC.renderSection('COMPUTER USE', 'Control how agents can use this computer', body); }; CC.renderLocalSettings = function () { @@ -530,33 +510,17 @@ window.__MOCK_STATE__ = { '' + '' ), - 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 + '
' + body + '
' + statusbar; + if (CC.modalMode) { + app.innerHTML = ''; + } else { + app.innerHTML = titlebar + '
' + body + '
' + 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 '

Local CloudCLI

Run the open-source app on this machine. No account required.

' + '
Local server
' + CC.esc(CC.localUrl(state) || 'Starts on demand') + '
' + '
' + - '
Computer Access
' + CC.esc(computerUseStatus(state).detail) + '
' + CC.esc(computerUseStatus(state).label) + '
' + - '
'; + '
Computer Use
' + CC.esc(computerUseStatus(state).detail) + '
' + CC.esc(computerUseStatus(state).label) + '
' + + '
'; } function envRow(environment) { diff --git a/electron/main.js b/electron/main.js index ea96a398..d6f493ed 100644 --- a/electron/main.js +++ b/electron/main.js @@ -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)); diff --git a/electron/preload.cjs b/electron/preload.cjs index a02b2d29..08132d64 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -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),