Add desktop app packaging and settings updates

This commit is contained in:
Simos Mikelatos
2026-06-17 22:15:36 +00:00
parent 6c2652aee6
commit 65fdc38f2e
14 changed files with 1153 additions and 119 deletions

View File

@@ -0,0 +1,61 @@
name: Desktop Windows Branch Build
on:
workflow_dispatch:
jobs:
build-windows:
name: Build unsigned Windows desktop artifact
runs-on: windows-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Resolve artifact metadata
id: artifact
shell: bash
run: |
SAFE_REF="$(printf '%s' "${GITHUB_REF_NAME}" | tr -c 'A-Za-z0-9._-' '-')"
echo "name=CloudCLI-windows-${SAFE_REF}-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Build unsigned Windows artifacts
run: npm run desktop:dist:win -- --publish never
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
- name: Verify Windows artifacts
shell: bash
run: |
test -n "$(find release -maxdepth 1 -name '*.exe' -print -quit)"
test -n "$(find release -maxdepth 1 -name '*.zip' -print -quit)"
sha256sum release/*.{exe,zip} > release/SHASUMS256.txt
cat release/SHASUMS256.txt
- name: Upload branch build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ steps.artifact.outputs.name }}
path: |
release/*.exe
release/*.zip
release/*.yml
release/*.blockmap
release/SHASUMS256.txt
if-no-files-found: error
retention-days: 14

View File

@@ -209,6 +209,11 @@ export class DesktopWindowManager {
this.mainWindow.webContents.send('cloudcli-desktop:state-updated', this.getDesktopState());
}
emitLauncherCommand(command) {
if (!this.mainWindow || this.mainWindow.webContents.isDestroyed()) return;
this.mainWindow.webContents.send('cloudcli-desktop:launcher-command', command);
}
async showTarget(target, { trackTab = true } = {}) {
if (!this.mainWindow) return;
if (trackTab) {
@@ -367,8 +372,8 @@ export class DesktopWindowManager {
label: 'Services',
submenu: [
{
label: 'Computer Use Preview',
click: () => void this.actions.showComputerUsePreview(),
label: 'Computer Access',
click: () => void this.actions.showComputerAccess(),
},
],
},
@@ -565,19 +570,10 @@ export class DesktopWindowManager {
this.tray.setContextMenu(Menu.buildFromTemplate(template));
}
async showDesktopAppMenu() {
async showDesktopSettings() {
if (!this.mainWindow) return this.getDesktopState();
const menu = Menu.buildFromTemplate([
{
label: 'Copy Diagnostics',
click: () => void this.actions.copyDiagnostics(),
},
{
label: 'Computer Use Preview',
click: () => void this.actions.showComputerUsePreview(),
},
]);
menu.popup({ window: this.mainWindow });
await this.showLauncher();
this.emitLauncherCommand({ type: 'open-sheet', sheet: 'app-settings' });
return this.getDesktopState();
}

File diff suppressed because one or more lines are too long

View File

@@ -3,11 +3,12 @@ window.__MOCK_STATE__ = {
account: { connected: true, email: 'you@cloudcli.ai' },
activeTarget: { kind: 'launcher', name: 'Launcher', url: null },
cloudLoading: false,
desktopSettings: { keepLocalServerRunning: false, exposeLocalServerOnNetwork: false },
desktopSettings: { keepLocalServerRunning: false, exposeLocalServerOnNetwork: false, themeMode: 'system' },
localWebUrl: 'http://localhost:3001',
shareableWebUrl: 'http://localhost:3001',
localServerRunning: false,
localStartupLogs: [],
computerUse: { enabled: false, consentMode: 'ask', running: false, connectedCount: 0, targetCount: 0 },
environments: [
{ id: 'env-api', name: 'api-gateway', subdomain: 'api-gateway', access_url: 'https://api-gateway.cloudcli.ai', status: 'running', region: 'fra1', agent: 'Claude Code' },
{ id: 'env-web', name: 'web-frontend', subdomain: 'web-frontend', access_url: 'https://web-frontend.cloudcli.ai', status: 'stopped', region: 'sfo1', agent: 'Codex' },
@@ -44,10 +45,10 @@ window.__MOCK_STATE__ = {
},
refreshEnvironments: function () { return Promise.resolve(clone(mockState)); },
copyDiagnostics: function () { return Promise.resolve(clone(mockState)); },
showComputerUsePreview: function () { return Promise.resolve(clone(mockState)); },
showComputerAccess: function () { return Promise.resolve(clone(mockState)); },
showEnvironmentPicker: function () { return Promise.resolve(clone(mockState)); },
showLauncher: function () { return Promise.resolve(clone(mockState)); },
showDesktopAppMenu: function () { return Promise.resolve(clone(mockState)); },
showDesktopSettings: 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)); },
@@ -59,7 +60,17 @@ window.__MOCK_STATE__ = {
},
updateSetting: function (key, value) {
mockState.desktopSettings = mockState.desktopSettings || {};
mockState.desktopSettings[key] = !!value;
mockState.desktopSettings[key] = key === 'themeMode' ? value : !!value;
return Promise.resolve(clone(mockState));
},
updateComputerUse: function (settings) {
mockState.computerUse = mockState.computerUse || { enabled: false, consentMode: 'ask', running: false, connectedCount: 0, targetCount: 0 };
if (typeof settings.enabled === 'boolean') mockState.computerUse.enabled = settings.enabled;
if (settings.consentMode === 'auto' || settings.consentMode === 'ask') mockState.computerUse.consentMode = settings.consentMode;
mockState.computerUse.running = mockState.computerUse.enabled;
return Promise.resolve(clone(mockState));
},
showComputerAccessPermissions: function () {
return Promise.resolve(clone(mockState));
},
openEnvironment: function (id) {
@@ -144,6 +155,30 @@ window.__MOCK_STATE__ = {
return error && error.message ? error.message : String(error);
}
function themeLabel(mode) {
if (mode === 'light') return 'Light';
if (mode === 'dark') return 'Dark';
return 'System';
}
function resolveTheme(state) {
var settings = state && state.desktopSettings ? state.desktopSettings : {};
var mode = settings.themeMode || 'system';
if (mode === 'light' || mode === 'dark') return mode;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function computerUseStatus(state) {
var computerUse = state && state.computerUse ? state.computerUse : {};
if (!computerUse.enabled) {
return { label: 'Off', 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: 'Ready · Ask first', tone: 'ok', detail: 'CloudCLI can use this computer. Agents need approval before control starts.' };
}
var CC = {
icon: icon,
esc: esc,
@@ -172,7 +207,19 @@ window.__MOCK_STATE__ = {
CC.setState = function (state) {
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);
}
};
CC.applyTheme = function (state) {
var settings = state && state.desktopSettings ? state.desktopSettings : {};
var themeMode = settings.themeMode || 'system';
var resolvedTheme = resolveTheme(state);
document.documentElement.setAttribute('data-theme', resolvedTheme);
document.documentElement.setAttribute('data-theme-mode', themeMode);
};
CC.refresh = function () {
@@ -268,12 +315,30 @@ window.__MOCK_STATE__ = {
return CC.run('Copied local URL to clipboard', function () { return bridge.copyLocalWebUrl(); });
case 'diagnostics':
return CC.run('Copied diagnostics to clipboard', function () { return bridge.copyDiagnostics(); });
case 'computer-use':
return CC.run('Opening Computer Use preview...', function () { return bridge.showComputerUsePreview(); });
case 'set-setting':
return CC.run('Saved', function () { return bridge.updateSetting(node.key, node.value); });
case 'set-theme-mode':
return CC.run('Saved', function () { return bridge.updateSetting('themeMode', node.value); });
case 'set-computer-mode':
return CC.run('Saved', function () {
return bridge.updateComputerUse({
enabled: true,
consentMode: node.value,
});
});
case 'set-computer-enabled':
return CC.run('Saved', function () {
var current = (CC.state && CC.state.computerUse) || { consentMode: 'ask' };
return bridge.updateComputerUse({
enabled: !!node.value,
consentMode: current.consentMode === 'auto' ? 'auto' : 'ask',
});
});
case 'computer-permissions':
return CC.run('Opening system permissions...', function () { return bridge.showComputerAccessPermissions(); });
case 'settings-toggle':
return CC.run('Opening settings...', function () { return bridge.showDesktopAppMenu(); });
CC.openSheet('app-settings');
return;
case 'dashboard':
return CC.run('Opening CloudCLI dashboard...', function () { return bridge.openCloudDashboard(); });
case 'env-action':
@@ -283,11 +348,13 @@ window.__MOCK_STATE__ = {
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.renderLocalSettings();
overlay.classList.toggle('open');
CC.openSheet('local-settings');
return;
case 'computer-settings-toggle':
CC.openSheet('computer-access');
return;
case 'settings-close':
overlay.classList.remove('open');
CC.closeSheet();
return;
default:
return;
@@ -334,22 +401,164 @@ window.__MOCK_STATE__ = {
'</div>';
};
CC.renderLocalSettings = function () {
var state = CC.state || {};
var settings = state.desktopSettings || {};
var url = localUrl(state) || 'starts on demand';
CC.renderSheet = function (title, subtitle, sections, footer) {
overlay.innerHTML =
'<div class="cc-sheet cc-modal">' +
'<div class="cc-sheet-h"><span class="lbl">Local Settings</span><button class="icon-btn" data-cc-action="settings-close">' + icon('x', 16) + '</button></div>' +
'<div class="cc-grp"><div class="lbl">Local server</div>' +
'<div class="cc-meta mono">' + esc(url) + '</div>' +
'<div class="cc-row2"><button class="btn sm" data-cc-action="open-web">' + icon('arrow', 14) + 'Open in browser</button><button class="btn sm" data-cc-action="copy-web">' + icon('copy', 14) + 'Copy URL</button></div>' +
'<label class="cc-toggle"><input type="checkbox" data-cc-setting="keepLocalServerRunning"' + (settings.keepLocalServerRunning ? ' checked' : '') + '><span><b>Keep server running</b><br>Leave Local CloudCLI available after you quit the app.</span></label>' +
'<label class="cc-toggle"><input type="checkbox" data-cc-setting="exposeLocalServerOnNetwork"' + (settings.exposeLocalServerOnNetwork ? ' checked' : '') + '><span><b>Allow LAN access</b><br>Use the copied URL from another device on this network.</span></label>' +
'<div class="cc-sheet-header">' +
'<div class="cc-sheet-copy"><div class="cc-sheet-title">' + esc(title) + '</div><div class="cc-sheet-subtitle">' + esc(subtitle || '') + '</div></div>' +
'<button class="icon-btn cc-sheet-close" data-cc-action="settings-close" title="Close">' + icon('x', 16) + '</button>' +
'</div>' +
'<div class="cc-sheet-body">' + sections.join('') + '</div>' +
(footer ? '<div class="cc-sheet-footer">' + footer + '</div>' : '') +
'</div>';
};
CC.renderSection = function (eyebrow, title, body) {
return '<section class="cc-section">' +
'<div class="cc-section-head">' +
'<div class="lbl">' + esc(eyebrow) + '</div>' +
'<div class="cc-section-title">' + esc(title) + '</div>' +
'</div>' +
'<div class="cc-section-body">' + body + '</div>' +
'</section>';
};
CC.renderRadioOption = function (name, value, checked, title, description) {
return '<label class="cc-choice">' +
'<input type="radio" name="' + esc(name) + '" value="' + esc(value) + '"' + (checked ? ' checked' : '') + '>' +
'<span><b>' + esc(title) + '</b><br>' + esc(description) + '</span>' +
'</label>';
};
CC.openSheet = function (sheet) {
if (sheet === 'app-settings') {
CC.renderAppSettings();
} else if (sheet === 'computer-access') {
CC.renderComputerAccess();
} else {
CC.renderLocalSettings();
}
CC.ui.openSheet = sheet;
overlay.classList.add('open');
};
CC.closeSheet = function () {
CC.ui.openSheet = null;
overlay.classList.remove('open');
};
CC.buildLocalServerSection = function (state, options) {
options = options || {};
var settings = state.desktopSettings || {};
var url = localUrl(state) || 'starts on demand';
var body = '<div class="cc-surface">' +
'<div class="cc-meta mono">' + esc(url) + '</div>' +
'<div class="cc-row2"><button class="btn sm" data-cc-action="open-web">' + icon('arrow', 14) + 'Open in browser</button><button class="btn sm" data-cc-action="copy-web">' + icon('copy', 14) + 'Copy URL</button></div>';
if (options.includePreferences) {
body +=
'<label class="cc-toggle"><input type="checkbox" data-cc-setting="keepLocalServerRunning"' + (settings.keepLocalServerRunning ? ' checked' : '') + '><span><b>Keep server running</b><br>Leave Local CloudCLI available after you quit the app.</span></label>' +
'<label class="cc-toggle"><input type="checkbox" data-cc-setting="exposeLocalServerOnNetwork"' + (settings.exposeLocalServerOnNetwork ? ' checked' : '') + '><span><b>Allow LAN access</b><br>Use the copied URL from another device on this network.</span></label>';
}
body += '</div>';
return CC.renderSection(
options.eyebrow || 'LOCAL SERVER',
options.title || 'Run Local CloudCLI on this machine',
body
);
};
CC.buildThemeSection = function (state) {
var settings = state.desktopSettings || {};
return CC.renderSection('APPEARANCE', 'Desktop theme', '' +
'<div class="cc-surface cc-choice-group">' +
CC.renderRadioOption('desktop-theme', 'system', settings.themeMode === 'system', 'System', 'Follow the operating system appearance.') +
CC.renderRadioOption('desktop-theme', 'light', settings.themeMode === 'light', 'Light', 'Use the light interface appearance.') +
CC.renderRadioOption('desktop-theme', 'dark', settings.themeMode === 'dark', 'Dark', 'Use the dark interface appearance.') +
'</div>'
);
};
CC.buildComputerAccessSections = function (state, options) {
options = options || {};
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', '' +
'<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>';
}
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;
};
CC.renderLocalSettings = function () {
var state = CC.state || {};
var sections = [
CC.buildLocalServerSection(state, { includePreferences: false }),
CC.renderSection('PREFERENCES', 'How the local service behaves', '' +
'<div class="cc-surface">' +
'<label class="cc-toggle"><input type="checkbox" data-cc-setting="keepLocalServerRunning"' + ((state.desktopSettings || {}).keepLocalServerRunning ? ' checked' : '') + '><span><b>Keep server running</b><br>Leave Local CloudCLI available after you quit the app.</span></label>' +
'<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.renderAppSettings = function () {
var state = CC.state || {};
var sections = [
CC.buildLocalServerSection(state, {
eyebrow: 'GENERAL',
title: 'Local CloudCLI',
includePreferences: true,
}),
CC.buildThemeSection(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.render = function (state) {
state = state || CC.state;
var titlebar = (CC._reg.titlebar || CC.titlebar)(state);
@@ -387,7 +596,7 @@ window.__MOCK_STATE__ = {
return;
}
if (overlay.classList.contains('open') && !event.target.closest('.cc-sheet')) {
overlay.classList.remove('open');
CC.closeSheet();
}
});
@@ -398,12 +607,27 @@ window.__MOCK_STATE__ = {
key: setting.getAttribute('data-cc-setting'),
value: setting.checked,
});
return;
}
var theme = event.target.closest('[name="desktop-theme"]');
if (theme) {
CC.act('set-theme-mode', { value: theme.value });
return;
}
var computerMode = event.target.closest('[name="computer-access-mode"]');
if (computerMode) {
CC.act('set-computer-mode', { value: computerMode.value });
return;
}
var computerEnabled = event.target.closest('[data-cc-computer-enabled]');
if (computerEnabled) {
CC.act('set-computer-enabled', { value: computerEnabled.checked });
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && overlay.classList.contains('open')) {
overlay.classList.remove('open');
CC.closeSheet();
return;
}
if ((event.metaKey || event.ctrlKey) && event.key === ',') {
@@ -429,9 +653,21 @@ window.__MOCK_STATE__ = {
document.body.classList.add(CC.platform);
wireEvents();
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
CC.applyTheme(CC.state);
});
}
if (bridge.onStateUpdated) {
bridge.onStateUpdated(function (state) { CC.setState(state); });
}
if (bridge.onLauncherCommand) {
bridge.onLauncherCommand(function (command) {
if (command && command.type === 'open-sheet') {
CC.openSheet(command.sheet);
}
});
}
CC.refresh().catch(function (error) {
CC._status = { msg: errMsg(error), tone: 'error' };
CC.render(CC.state);
@@ -462,7 +698,9 @@ window.__MOCK_STATE__ = {
function localPane(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-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>';
}
function envRow(environment) {

View File

@@ -222,6 +222,7 @@ export class LocalServerController {
this.desktopSettings = {
keepLocalServerRunning: false,
exposeLocalServerOnNetwork: false,
themeMode: 'system',
};
}
@@ -295,11 +296,13 @@ export class LocalServerController {
this.desktopSettings = {
keepLocalServerRunning: Boolean(stored.keepLocalServerRunning),
exposeLocalServerOnNetwork: Boolean(stored.exposeLocalServerOnNetwork),
themeMode: stored.themeMode === 'light' || stored.themeMode === 'dark' ? stored.themeMode : 'system',
};
} catch {
this.desktopSettings = {
keepLocalServerRunning: false,
exposeLocalServerOnNetwork: false,
themeMode: 'system',
};
}
}
@@ -308,6 +311,7 @@ export class LocalServerController {
this.desktopSettings = {
keepLocalServerRunning: Boolean(nextSettings.keepLocalServerRunning),
exposeLocalServerOnNetwork: Boolean(nextSettings.exposeLocalServerOnNetwork),
themeMode: nextSettings.themeMode === 'light' || nextSettings.themeMode === 'dark' ? nextSettings.themeMode : 'system',
};
await fs.mkdir(path.dirname(this.settingsPath), { recursive: true });
await fs.writeFile(this.settingsPath, JSON.stringify(this.desktopSettings, null, 2), 'utf8');
@@ -321,7 +325,8 @@ export class LocalServerController {
const wasExposeSetting = key === 'exposeLocalServerOnNetwork';
const wasLocalRunning = Boolean(this.localServerUrl);
await this.saveDesktopSettings({ ...this.desktopSettings, [key]: Boolean(value) });
const nextValue = key === 'themeMode' ? value : Boolean(value);
await this.saveDesktopSettings({ ...this.desktopSettings, [key]: nextValue });
return {
desktopSettings: this.desktopSettings,

View File

@@ -71,7 +71,7 @@ async function promptComputerUseConsent(sessionId) {
buttons: ['Allow this session', 'Deny'],
defaultId: 0,
cancelId: 1,
title: 'Computer Use request',
title: 'Computer Access 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,7 +248,7 @@ async function copyDiagnostics() {
});
}
async function showMacComputerUsePermissions() {
async function showMacComputerAccessPermissions() {
if (process.platform !== 'darwin') return;
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
const accessibilityTrusted = systemPreferences.isTrustedAccessibilityClient(false);
@@ -256,7 +256,7 @@ async function showMacComputerUsePermissions() {
`Screen Recording: ${screenStatus === 'granted' ? 'granted' : 'not granted'}`,
`Accessibility: ${accessibilityTrusted ? 'granted' : 'not granted'}`,
'',
'Computer Use needs both permissions to capture the screen and control the mouse and keyboard.',
'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');
@@ -265,8 +265,8 @@ async function showMacComputerUsePermissions() {
buttons: ['Open Screen Recording', 'Open Accessibility', 'Close'],
defaultId: 0,
cancelId: 2,
title: 'Computer Use Permissions',
message: 'Grant macOS permissions for Computer Use',
title: 'Computer Access Permissions',
message: 'Grant macOS permissions for Computer Access',
detail,
});
@@ -277,58 +277,21 @@ async function showMacComputerUsePermissions() {
}
}
// Desktop control for cloud Computer Use: the desktop acts as a TeamViewer-style
// agent for hosted environments. Enabling here lets cloud agents drive THIS
// machine; the user picks whether to auto-connect or be asked per session.
async function showComputerUsePreview() {
const state = computerAgent?.getState() || { enabled: false, consentMode: 'ask' };
const buttons = [];
const actions = [];
async function showComputerAccess() {
await desktopWindow?.showLauncher();
desktopWindow?.emitLauncherCommand({ type: 'open-sheet', sheet: 'computer-access' });
return getDesktopState();
}
if (!state.enabled) {
buttons.push('Enable — ask each session'); actions.push({ kind: 'enable', consentMode: 'ask' });
buttons.push('Enable — auto-connect'); actions.push({ kind: 'enable', consentMode: 'auto' });
} else {
buttons.push('Disable Computer Use'); actions.push({ kind: 'disable' });
const otherMode = state.consentMode === 'auto' ? 'ask' : 'auto';
buttons.push(`Switch to ${otherMode === 'auto' ? 'auto-connect' : 'ask each session'}`);
actions.push({ kind: 'enable', consentMode: otherMode });
}
if (process.platform === 'darwin') {
buttons.push('macOS Permissions…'); actions.push({ kind: 'permissions' });
}
buttons.push('Close'); actions.push({ kind: 'close' });
const statusLine = state.enabled
? `Enabled — ${state.consentMode === 'auto' ? 'auto-connect' : 'ask each session'} · ${state.connectedCount || 0} environment(s) linked`
: 'Disabled';
const { response } = await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, {
type: 'question',
buttons,
defaultId: 0,
cancelId: buttons.length - 1,
title: 'Computer Use (Desktop Agent)',
message: 'Let cloud agents control this computer',
detail: [
`Status: ${statusLine}`,
'',
'When enabled, agents running in your CloudCLI cloud environments can see this screen and drive its mouse and keyboard.',
'• Ask each session: you approve a prompt the first time each session wants control.',
'• Auto-connect: sessions can act without a prompt.',
process.platform === 'linux' ? '\nLinux needs X utilities (libxtst, imagemagick) installed to capture the screen and drive input.' : '',
].join('\n'),
});
const action = actions[response];
if (!action) return;
if (action.kind === 'enable') {
await computerAgent?.saveSettings({ enabled: true, consentMode: action.consentMode });
} else if (action.kind === 'disable') {
await computerAgent?.saveSettings({ enabled: false, consentMode: state.consentMode });
} else if (action.kind === 'permissions') {
await showMacComputerUsePermissions();
}
async function updateComputerUse(settings) {
const current = computerAgent?.getSettings() || { enabled: false, consentMode: 'ask' };
const next = {
enabled: typeof settings?.enabled === 'boolean' ? settings.enabled : current.enabled,
consentMode: settings?.consentMode === 'auto' ? 'auto' : 'ask',
};
await computerAgent?.saveSettings(next);
syncDesktopState();
return getDesktopState();
}
async function refreshCloudEnvironments({ showErrors = false } = {}) {
@@ -725,11 +688,16 @@ function registerIpcHandlers() {
await desktopWindow.showLauncher();
return getDesktopState();
});
ipcMain.handle('cloudcli-desktop:show-computer-use-preview', async () => {
await showComputerUsePreview();
ipcMain.handle('cloudcli-desktop:show-computer-access', async () => {
await showComputerAccess();
return getDesktopState();
});
ipcMain.handle('cloudcli-desktop:show-desktop-app-menu', async () => desktopWindow.showDesktopAppMenu());
ipcMain.handle('cloudcli-desktop:update-computer-use', async (_event, settings) => updateComputerUse(settings));
ipcMain.handle('cloudcli-desktop:show-computer-use-permissions', async () => {
await showMacComputerAccessPermissions();
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));
@@ -812,7 +780,7 @@ async function createDesktopWindow() {
openCloudDashboard,
refreshCloudEnvironments: () => refreshCloudEnvironments({ showErrors: true }),
setActiveTarget,
showComputerUsePreview,
showComputerAccess,
showEnvironmentPicker,
showError,
startEnvironment,

View File

@@ -14,8 +14,10 @@ if (window.location.protocol === 'file:') {
refreshEnvironments: () => ipcRenderer.invoke('cloudcli-desktop:refresh-environments'),
showEnvironmentPicker: () => ipcRenderer.invoke('cloudcli-desktop:show-environment-picker'),
showLauncher: () => ipcRenderer.invoke('cloudcli-desktop:show-launcher'),
showComputerUsePreview: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-use-preview'),
showDesktopAppMenu: () => ipcRenderer.invoke('cloudcli-desktop:show-desktop-app-menu'),
showComputerAccess: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-access'),
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'),
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),
@@ -24,5 +26,8 @@ if (window.location.protocol === 'file:') {
onStateUpdated: (callback) => {
ipcRenderer.on('cloudcli-desktop:state-updated', (_event, state) => callback(state));
},
onLauncherCommand: (callback) => {
ipcRenderer.on('cloudcli-desktop:launcher-command', (_event, command) => callback(command));
},
});
}

View File

@@ -36,6 +36,7 @@
"desktop:dev": "cross-env ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js",
"desktop:pack": "npm run build && electron-builder --dir",
"desktop:dist:mac": "npm run build && electron-builder --mac dmg zip",
"desktop:dist:win": "npm run build && electron-builder --win nsis zip",
"desktop:icon:mac": "node electron/scripts/generate-macos-icon.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build",
@@ -100,6 +101,12 @@
}
]
}
},
"win": {
"target": [
"nsis",
"zip"
]
}
},
"keywords": [

View File

@@ -264,7 +264,7 @@ export default function SidebarContent({
<div key={projectResult.projectName} className="space-y-1">
<div className="flex items-center gap-1.5 px-1 py-1">
<Folder className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<span className="truncate text-xs font-medium text-foreground">
<span className="truncate text-xs font-normal text-foreground">
{projectResult.projectDisplayName}
</span>
</div>
@@ -284,7 +284,7 @@ export default function SidebarContent({
>
<div className="mb-1 flex items-center gap-1.5">
<MessageSquare className="h-3 w-3 flex-shrink-0 text-primary" />
<span className="truncate text-xs font-medium text-foreground">
<span className="truncate text-xs font-normal text-foreground">
{session.sessionSummary}
</span>
{session.provider && session.provider !== 'claude' && (
@@ -296,7 +296,7 @@ export default function SidebarContent({
<div className="space-y-1 pl-4">
{session.matches.map((match, idx) => (
<div key={idx} className="flex items-start gap-1">
<span className="mt-0.5 flex-shrink-0 text-[10px] font-medium uppercase text-muted-foreground/60">
<span className="mt-0.5 flex-shrink-0 text-[10px] font-normal uppercase text-muted-foreground/60">
{match.role === 'user' ? 'U' : 'A'}
</span>
<HighlightedSnippet
@@ -334,11 +334,11 @@ export default function SidebarContent({
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<Activity className="h-3.5 w-3.5" />
</span>
<span className="truncate text-xs font-medium text-foreground">
<span className="truncate text-xs font-normal text-foreground">
{t('running.title', 'Running now')}
</span>
</div>
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-700 dark:text-emerald-300">
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] font-normal text-emerald-700 dark:text-emerald-300">
{runningSessionsCount}
</span>
</div>
@@ -393,7 +393,7 @@ export default function SidebarContent({
<div className="min-w-0">
<div className="flex items-center gap-2">
<Folder className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<span className="truncate text-sm font-medium text-foreground">
<span className="truncate text-sm font-normal text-foreground">
{project.displayName}
</span>
<span className="inline-flex items-center justify-center rounded-full bg-muted px-1 py-px text-center text-[7px] font-medium uppercase leading-none tracking-[0.02em] text-muted-foreground">
@@ -446,7 +446,7 @@ export default function SidebarContent({
<SessionProviderLogo provider={session.__provider} className="h-3.5 w-3.5 flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate text-xs font-medium text-foreground">
<span className="truncate text-xs font-normal text-foreground">
{(typeof session.summary === 'string' && session.summary.trim().length > 0
? session.summary
: typeof session.name === 'string' && session.name.trim().length > 0
@@ -482,7 +482,7 @@ export default function SidebarContent({
<div className="min-w-0">
<div className="flex items-center gap-2">
<Folder className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<span className="truncate text-sm font-medium text-foreground">
<span className="truncate text-sm font-normal text-foreground">
{group.projectDisplayName}
</span>
{group.isProjectArchived && (
@@ -511,7 +511,7 @@ export default function SidebarContent({
<SessionProviderLogo provider={session.provider} className="h-3.5 w-3.5 flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate text-xs font-medium text-foreground">
<span className="truncate text-xs font-normal text-foreground">
{session.sessionTitle}
</span>
{session.lastActivity && (

View File

@@ -52,7 +52,7 @@ export default function SidebarFooter({
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
</div>
<div className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-blue-600 dark:text-blue-300">
<span className="block truncate text-sm font-normal text-blue-600 dark:text-blue-300">
{releaseInfo?.title || `v${latestVersion}`}
</span>
<span className="text-[10px] text-blue-500/70 dark:text-blue-400/60">
@@ -73,7 +73,7 @@ export default function SidebarFooter({
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
</div>
<div className="min-w-0 flex-1 text-left">
<span className="block truncate text-sm font-medium text-blue-600 dark:text-blue-300">
<span className="block truncate text-sm font-normal text-blue-600 dark:text-blue-300">
{releaseInfo?.title || `v${latestVersion}`}
</span>
<span className="text-xs text-blue-500/70 dark:text-blue-400/60">
@@ -150,7 +150,7 @@ export default function SidebarFooter({
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-background/80">
<Bug className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-sm font-medium text-foreground">{t('actions.reportIssue')}</span>
<span className="text-sm font-normal text-foreground">{t('actions.reportIssue')}</span>
</a>
</div>
@@ -165,7 +165,7 @@ export default function SidebarFooter({
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-background/80">
<DiscordIcon className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-sm font-medium text-foreground">{t('actions.joinCommunity')}</span>
<span className="text-sm font-normal text-foreground">{t('actions.joinCommunity')}</span>
</a>
</div>
@@ -178,7 +178,7 @@ export default function SidebarFooter({
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-background/80">
<Settings className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-sm font-medium text-foreground">{t('actions.settings')}</span>
<span className="text-sm font-normal text-foreground">{t('actions.settings')}</span>
</button>
</div>
</div>

View File

@@ -167,7 +167,7 @@ export default function SidebarHeader({
aria-label={t('search.runningTooltip', 'Running sessions')}
title={t('search.runningTooltip', 'Running sessions')}
className={cn(
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all",
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
searchMode === 'running'
? "bg-background shadow-sm text-foreground ring-1 ring-emerald-500/15"
: "text-muted-foreground hover:text-foreground"
@@ -307,7 +307,7 @@ export default function SidebarHeader({
aria-label={t('search.runningTooltip', 'Running sessions')}
title={t('search.runningTooltip', 'Running sessions')}
className={cn(
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all",
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all",
searchMode === 'running'
? "bg-background shadow-sm text-foreground ring-1 ring-emerald-500/15"
: "text-muted-foreground hover:text-foreground"

View File

@@ -102,7 +102,7 @@ export default function TaskIndicator({
title={indicatorConfig.title}
>
<Icon className={sizeClassNames[size]} />
<span className="font-medium">{indicatorConfig.label}</span>
<span className="font-normal">{indicatorConfig.label}</span>
</div>
);
}

View File

@@ -91,4 +91,4 @@ export const ThemeProvider = ({ children }) => {
{children}
</ThemeContext.Provider>
);
};
};

View File

@@ -129,6 +129,8 @@
body {
@apply bg-background text-foreground;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 0;
}