feat: improve Computer Use linking status

This commit is contained in:
Simos Mikelatos
2026-06-19 13:47:16 +00:00
parent 218e8e2e38
commit 4d70a2588c
14 changed files with 474 additions and 231 deletions

View File

@@ -1,45 +1,9 @@
import { BrowserView, BrowserWindow, Menu, Tray, nativeImage, nativeTheme, session } from 'electron';
import { BrowserWindow, Menu, Tray, nativeImage, nativeTheme, session } from 'electron';
import { ViewHost } from './viewHost.js';
const TITLEBAR_HEIGHT = 44;
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function buildPlaceholderHtml(title, message, logs = []) {
const logHtml = logs.length
? `<pre>${logs.map(escapeHtml).join('\n')}</pre>`
: '<pre>Waiting for process output...</pre>';
return [
'<!doctype html><meta charset="utf-8">',
'<style>',
'html,body{margin:0;height:100%;background:#0a0a0a;color:#fafafa;font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}',
'body{padding:28px;overflow:hidden}',
'.shell{height:100%;display:flex;flex-direction:column;gap:16px}',
'.box{display:flex;align-items:center;gap:10px;color:#d4d4d4;flex:0 0 auto}',
'.dot{width:8px;height:8px;border-radius:50%;background:#0b60ea;box-shadow:0 0 0 6px rgba(11,96,234,.15)}',
'pre{margin:0;flex:1;overflow:auto;border:1px solid #262626;border-radius:10px;background:#050505;color:#d4d4d4;padding:14px;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:pre-wrap;user-select:text}',
'</style>',
'<div class="shell">',
`<div class="box"><span class="dot"></span><span>${escapeHtml(message || `Opening ${title}...`)}</span></div>`,
logHtml,
'</div>',
].join('');
}
function isHttpUrl(url) {
try {
const parsed = new URL(url);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
function isAllowedPermissionOrigin(sourceUrl, controlPlaneUrl) {
try {
const source = new URL(sourceUrl);
@@ -88,8 +52,14 @@ export class DesktopWindowManager {
this.settingsWindow = null;
this.tray = null;
this.launcherLoaded = false;
this.activeContentView = null;
this.tabViews = new Map();
this.viewHost = new ViewHost({
appName: this.appName,
getMainWindow: () => this.mainWindow,
getContentViewBounds: () => this.getContentViewBounds(),
getPreloadPath: this.getPreloadPath,
openExternalUrl: this.openExternalUrl,
showError: this.actions.showError,
});
}
getMainWindow() {
@@ -112,125 +82,27 @@ export class DesktopWindowManager {
};
}
configureChildWebContents(webContents) {
webContents.setWindowOpenHandler(({ url }) => {
void this.openExternalUrl(url).catch((error) => this.actions.showError('Could not open external link', error));
return { action: 'deny' };
});
}
detachActiveContentView() {
if (!this.mainWindow || this.mainWindow.isDestroyed() || !this.activeContentView) return;
try {
if (this.mainWindow.getBrowserViews().includes(this.activeContentView)) {
this.mainWindow.removeBrowserView(this.activeContentView);
}
} catch {
// BrowserViews may already be gone during BrowserWindow teardown.
}
this.activeContentView = null;
}
getOrCreateTabView(tabId) {
let view = this.tabViews.get(tabId);
if (view) return view;
view = new BrowserView({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: this.getPreloadPath(),
},
});
this.configureChildWebContents(view.webContents);
this.tabViews.set(tabId, view);
return view;
}
attachContentView(view) {
if (!this.mainWindow || this.mainWindow.isDestroyed()) return;
if (this.activeContentView && this.activeContentView !== view) {
this.detachActiveContentView();
}
this.activeContentView = view;
try {
if (!this.mainWindow.getBrowserViews().includes(view)) {
this.mainWindow.addBrowserView(view);
}
} catch {
return;
}
view.setBounds(this.getContentViewBounds());
view.setAutoResize({ width: true, height: true });
this.viewHost.detachAll();
}
async showTabPlaceholder(target, message) {
const tabId = this.tabs.getTabIdForTarget(target);
const view = this.getOrCreateTabView(tabId);
this.attachContentView(view);
const html = buildPlaceholderHtml(target.name || this.appName, message);
await view.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
view.__cloudcliStartupHtml = html;
view.__cloudcliLoadedUrl = null;
await this.viewHost.showTabPlaceholder(tabId, target, message);
}
async showLocalStartupTarget(target, logs) {
const tabId = this.tabs.getTabIdForTarget(target);
const view = this.getOrCreateTabView(tabId);
if (view.__cloudcliLoadingUrl) return;
this.attachContentView(view);
const html = buildPlaceholderHtml(target.name || this.appName, 'Starting Local CloudCLI...', logs);
if (view.__cloudcliStartupHtml === html) return;
await view.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
view.__cloudcliStartupHtml = html;
view.__cloudcliLoadedUrl = null;
await this.viewHost.showLocalStartupTarget(tabId, target, logs);
}
async showContentTarget(target) {
if (!isHttpUrl(target.url)) {
throw new Error(`Refusing to load unsupported app URL: ${target.url}`);
}
const tabId = this.tabs.getTabIdForTarget(target);
const view = this.getOrCreateTabView(tabId);
this.attachContentView(view);
if (view.__cloudcliLoadedUrl !== target.url) {
view.__cloudcliLoadingUrl = target.url;
try {
await view.webContents.loadURL(target.url);
view.__cloudcliLoadedUrl = target.url;
view.__cloudcliStartupHtml = null;
} finally {
if (view.__cloudcliLoadingUrl === target.url) {
view.__cloudcliLoadingUrl = null;
}
}
}
await this.viewHost.showContentTarget(tabId, target);
}
destroyTabView(tabId) {
const view = this.tabViews.get(tabId);
if (!view) return;
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
try {
if (this.mainWindow.getBrowserViews().includes(view)) {
this.mainWindow.removeBrowserView(view);
}
} catch {
// Ignore teardown races; Electron owns final destruction during quit.
}
}
if (this.activeContentView === view) {
this.activeContentView = null;
}
try {
if (!view.webContents.isDestroyed()) {
view.webContents.destroy();
}
} catch {
// The view may already be destroyed by its parent BrowserWindow.
}
this.tabViews.delete(tabId);
this.viewHost.destroyTabView(tabId);
}
emitDesktopState() {
@@ -270,7 +142,6 @@ export class DesktopWindowManager {
this.settingsWindow = new BrowserWindow({
parent: this.mainWindow,
modal: true,
show: false,
frame: false,
transparent: true,
@@ -290,7 +161,7 @@ export class DesktopWindowManager {
},
});
this.syncSettingsWindowBounds();
this.configureChildWebContents(this.settingsWindow.webContents);
this.viewHost.configureChildWebContents(this.settingsWindow.webContents);
this.settingsWindow.once('ready-to-show', () => this.settingsWindow?.show());
this.settingsWindow.on('closed', () => {
this.settingsWindow = null;
@@ -326,6 +197,7 @@ export class DesktopWindowManager {
this.detachActiveContentView();
this.buildAppMenu();
this.mainWindow.setTitle(this.appName);
this.mainWindow.webContents.focus();
if (!this.launcherLoaded) {
await this.mainWindow.loadFile(this.getLauncherPath());
this.launcherLoaded = true;
@@ -757,9 +629,7 @@ export class DesktopWindowManager {
});
this.mainWindow.on('resize', () => {
if (this.activeContentView) {
this.activeContentView.setBounds(this.getContentViewBounds());
}
this.viewHost.resizeActiveView();
this.syncSettingsWindowBounds();
});
@@ -768,8 +638,7 @@ export class DesktopWindowManager {
});
this.mainWindow.on('closed', () => {
this.tabViews.clear();
this.activeContentView = null;
this.viewHost.clear();
this.settingsWindow = null;
this.mainWindow = null;
this.launcherLoaded = false;