mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-25 12:16:00 +08:00
fix: add Electron tab diagnostics
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, Menu, Tray, nativeImage, nativeTheme, session } from 'electron';
|
import { BrowserWindow, Menu, Tray, clipboard, nativeImage, nativeTheme, session, webContents as electronWebContents } from 'electron';
|
||||||
|
|
||||||
import { ViewHost } from './viewHost.js';
|
import { ViewHost } from './viewHost.js';
|
||||||
|
|
||||||
@@ -23,6 +23,13 @@ function isAllowedPermissionOrigin(sourceUrl, controlPlaneUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWebContentsProcessId(contents) {
|
||||||
|
return {
|
||||||
|
osProcessId: typeof contents.getOSProcessId === 'function' ? contents.getOSProcessId() : null,
|
||||||
|
processId: typeof contents.getProcessId === 'function' ? contents.getProcessId() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class DesktopWindowManager {
|
export class DesktopWindowManager {
|
||||||
constructor({
|
constructor({
|
||||||
appName,
|
appName,
|
||||||
@@ -226,6 +233,90 @@ export class DesktopWindowManager {
|
|||||||
return this.getDesktopState();
|
return this.getDesktopState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reloadActiveTab() {
|
||||||
|
const activeTab = this.tabs.getActiveTab();
|
||||||
|
if (!activeTab || activeTab.id === 'home' || activeTab.kind === 'launcher') {
|
||||||
|
this.emitDesktopState();
|
||||||
|
return this.getDesktopState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloaded = this.viewHost.reloadTab(activeTab.id);
|
||||||
|
if (!reloaded && activeTab.target?.url) {
|
||||||
|
await this.showTarget(activeTab.target, { trackTab: false });
|
||||||
|
}
|
||||||
|
this.emitDesktopState();
|
||||||
|
return this.getDesktopState();
|
||||||
|
}
|
||||||
|
|
||||||
|
openActiveTabDevTools() {
|
||||||
|
if (this.viewHost.openActiveViewDevTools()) return;
|
||||||
|
void this.actions.showError('No active BrowserView', new Error('Switch to a non-launcher tab before opening active tab DevTools.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadActiveBrowserViewForDiagnostics() {
|
||||||
|
if (this.viewHost.reloadActiveView()) return;
|
||||||
|
void this.actions.showError('No active BrowserView', new Error('Switch to a non-launcher tab before reloading the active BrowserView.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
detachActiveBrowserViewForDiagnostics() {
|
||||||
|
if (this.viewHost.detachActiveView()) return;
|
||||||
|
void this.actions.showError('No active BrowserView', new Error('Switch to a non-launcher tab before detaching the active BrowserView.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
copyWebContentsDiagnostics() {
|
||||||
|
const tabViewDiagnostics = this.viewHost.getTabViewDiagnostics();
|
||||||
|
const tabViewByContentsId = new Map(
|
||||||
|
tabViewDiagnostics
|
||||||
|
.filter((item) => item.webContentsId != null)
|
||||||
|
.map((item) => [item.webContentsId, item])
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = electronWebContents.getAllWebContents().map((contents) => {
|
||||||
|
const destroyed = contents.isDestroyed();
|
||||||
|
const processIds = destroyed ? { osProcessId: null, processId: null } : getWebContentsProcessId(contents);
|
||||||
|
const tabView = tabViewByContentsId.get(contents.id);
|
||||||
|
let owner = 'unknown';
|
||||||
|
if (this.mainWindow?.webContents?.id === contents.id) {
|
||||||
|
owner = 'main-window';
|
||||||
|
} else if (this.settingsWindow?.webContents?.id === contents.id) {
|
||||||
|
owner = 'settings-window';
|
||||||
|
} else if (tabView) {
|
||||||
|
owner = `browser-view:${tabView.tabId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: contents.id,
|
||||||
|
owner,
|
||||||
|
osProcessId: processIds.osProcessId,
|
||||||
|
processId: processIds.processId,
|
||||||
|
url: destroyed ? null : contents.getURL(),
|
||||||
|
title: destroyed ? null : contents.getTitle(),
|
||||||
|
destroyed,
|
||||||
|
focused: destroyed || typeof contents.isFocused !== 'function' ? false : contents.isFocused(),
|
||||||
|
attached: tabView ? tabView.attached : null,
|
||||||
|
active: tabView ? tabView.active : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTab = this.tabs.getActiveTab();
|
||||||
|
const diagnostics = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
activeTabId: this.tabs.activeTabId,
|
||||||
|
activeTab: activeTab
|
||||||
|
? {
|
||||||
|
id: activeTab.id,
|
||||||
|
title: activeTab.title,
|
||||||
|
kind: activeTab.kind,
|
||||||
|
targetUrl: activeTab.target?.url || null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
tabViews: tabViewDiagnostics,
|
||||||
|
webContents: rows,
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.writeText(JSON.stringify(diagnostics, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
async closeDesktopTab(tabId) {
|
async closeDesktopTab(tabId) {
|
||||||
const tab = this.tabs.remove(tabId);
|
const tab = this.tabs.remove(tabId);
|
||||||
if (!tab) return this.getDesktopState();
|
if (!tab) return this.getDesktopState();
|
||||||
@@ -426,8 +517,8 @@ export class DesktopWindowManager {
|
|||||||
enabled: Boolean(cloudState.account?.apiKey),
|
enabled: Boolean(cloudState.account?.apiKey),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Disconnect Cloud Account',
|
label: 'Logout CloudCLI Account',
|
||||||
click: () => void this.actions.clearCloudAccount().catch((error) => this.actions.showError('Could not disconnect cloud account', error)),
|
click: () => void this.actions.clearCloudAccount().catch((error) => this.actions.showError('Could not logout', error)),
|
||||||
enabled: Boolean(cloudState.account?.apiKey),
|
enabled: Boolean(cloudState.account?.apiKey),
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -455,6 +546,22 @@ export class DesktopWindowManager {
|
|||||||
{ role: 'reload' },
|
{ role: 'reload' },
|
||||||
{ role: 'forceReload' },
|
{ role: 'forceReload' },
|
||||||
{ role: 'toggleDevTools' },
|
{ role: 'toggleDevTools' },
|
||||||
|
{
|
||||||
|
label: 'Open Active Tab DevTools',
|
||||||
|
click: () => this.openActiveTabDevTools(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Copy WebContents Diagnostics',
|
||||||
|
click: () => this.copyWebContentsDiagnostics(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reload Active BrowserView',
|
||||||
|
click: () => this.reloadActiveBrowserViewForDiagnostics(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Detach Active BrowserView',
|
||||||
|
click: () => this.detachActiveBrowserViewForDiagnostics(),
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'resetZoom' },
|
{ role: 'resetZoom' },
|
||||||
{ role: 'zoomIn' },
|
{ role: 'zoomIn' },
|
||||||
@@ -523,8 +630,8 @@ export class DesktopWindowManager {
|
|||||||
click: () => void this.actions.connectCloudAccount().catch((error) => this.actions.showError('Could not connect CloudCLI account', error)),
|
click: () => void this.actions.connectCloudAccount().catch((error) => this.actions.showError('Could not connect CloudCLI account', error)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Disconnect Cloud Account',
|
label: 'Logout CloudCLI Account',
|
||||||
click: () => void this.actions.clearCloudAccount().catch((error) => this.actions.showError('Could not disconnect cloud account', error)),
|
click: () => void this.actions.clearCloudAccount().catch((error) => this.actions.showError('Could not logout', error)),
|
||||||
enabled: Boolean(cloudState.account?.apiKey),
|
enabled: Boolean(cloudState.account?.apiKey),
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
|||||||
@@ -51,7 +51,16 @@ window.__MOCK_STATE__ = {
|
|||||||
mockState.account = { connected: true, email: 'you@cloudcli.ai' };
|
mockState.account = { connected: true, email: 'you@cloudcli.ai' };
|
||||||
return Promise.resolve(clone(mockState));
|
return Promise.resolve(clone(mockState));
|
||||||
},
|
},
|
||||||
|
disconnectCloud: function () {
|
||||||
|
mockState.account = { connected: false, email: null };
|
||||||
|
mockState.environments = [];
|
||||||
|
mockState.tabs = (mockState.tabs || []).filter(function (tab) { return tab.kind !== 'remote'; });
|
||||||
|
mockState.activeTabId = 'home';
|
||||||
|
mockState.activeTarget = { kind: 'launcher', name: 'Launcher', url: null };
|
||||||
|
return Promise.resolve(clone(mockState));
|
||||||
|
},
|
||||||
refreshEnvironments: function () { return Promise.resolve(clone(mockState)); },
|
refreshEnvironments: function () { return Promise.resolve(clone(mockState)); },
|
||||||
|
refreshActiveTab: function () { return Promise.resolve(clone(mockState)); },
|
||||||
copyDiagnostics: function () { return Promise.resolve(clone(mockState)); },
|
copyDiagnostics: function () { return Promise.resolve(clone(mockState)); },
|
||||||
showComputerAccess: function () { return Promise.resolve(clone(mockState)); },
|
showComputerAccess: function () { return Promise.resolve(clone(mockState)); },
|
||||||
showEnvironmentPicker: function () { return Promise.resolve(clone(mockState)); },
|
showEnvironmentPicker: function () { return Promise.resolve(clone(mockState)); },
|
||||||
@@ -118,6 +127,7 @@ window.__MOCK_STATE__ = {
|
|||||||
monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
|
monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
|
||||||
phone: '<rect x="7" y="2" width="10" height="20" rx="2"/><line x1="11" y1="18" x2="13" y2="18"/>',
|
phone: '<rect x="7" y="2" width="10" height="20" rx="2"/><line x1="11" y1="18" x2="13" y2="18"/>',
|
||||||
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||||
|
logOut: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
|
||||||
};
|
};
|
||||||
var FILLED = { play: true };
|
var FILLED = { play: true };
|
||||||
|
|
||||||
@@ -330,6 +340,8 @@ window.__MOCK_STATE__ = {
|
|||||||
return CC.run('Starting Local CloudCLI...', function () { return bridge.openLocal(); });
|
return CC.run('Starting Local CloudCLI...', function () { return bridge.openLocal(); });
|
||||||
case 'connect':
|
case 'connect':
|
||||||
return CC.run('Opening cloudcli.ai to connect your account...', function () { return bridge.connectCloud(); });
|
return CC.run('Opening cloudcli.ai to connect your account...', function () { return bridge.connectCloud(); });
|
||||||
|
case 'logout':
|
||||||
|
return CC.run('Logging out...', function () { return bridge.disconnectCloud(); });
|
||||||
case 'open-web':
|
case 'open-web':
|
||||||
return CC.run('Opening local web UI in your browser...', function () { return bridge.openLocalWebUi(); });
|
return CC.run('Opening local web UI in your browser...', function () { return bridge.openLocalWebUi(); });
|
||||||
case 'copy-web':
|
case 'copy-web':
|
||||||
@@ -382,6 +394,8 @@ window.__MOCK_STATE__ = {
|
|||||||
return CC.run('Opening CloudCLI dashboard...', function () { return bridge.openCloudDashboard(); });
|
return CC.run('Opening CloudCLI dashboard...', function () { return bridge.openCloudDashboard(); });
|
||||||
case 'refresh-environments':
|
case 'refresh-environments':
|
||||||
return CC.run('Refreshing cloud environments...', function () { return bridge.refreshEnvironments(); });
|
return CC.run('Refreshing cloud environments...', function () { return bridge.refreshEnvironments(); });
|
||||||
|
case 'refresh-tab':
|
||||||
|
return CC.run('Refreshing tab...', function () { return bridge.refreshActiveTab(); });
|
||||||
case 'env-action':
|
case 'env-action':
|
||||||
return CC.run('Opening environment...', function () { return bridge.runActiveEnvironmentAction(node.getAttribute('data-cc-env-action')); });
|
return CC.run('Opening environment...', function () { return bridge.runActiveEnvironmentAction(node.getAttribute('data-cc-env-action')); });
|
||||||
case 'env-menu':
|
case 'env-menu':
|
||||||
@@ -408,14 +422,24 @@ window.__MOCK_STATE__ = {
|
|||||||
|
|
||||||
CC.titlebar = function (state) {
|
CC.titlebar = function (state) {
|
||||||
var conn = connected(state);
|
var conn = connected(state);
|
||||||
var activeRemote = state.activeTarget && state.activeTarget.kind === 'remote';
|
var activeTab = (state.tabs || []).filter(function (tab) { return tab.active; })[0] || null;
|
||||||
var envActions = activeRemote ? '<button class="btn sm tb-action no-drag" data-cc-action="env-menu" title="Open environment actions">Open environment in...</button>' : '';
|
var activeEnvironmentId = state.activeTarget && state.activeTarget.kind === 'remote' ? state.activeTarget.id : null;
|
||||||
|
if (!activeEnvironmentId && activeTab && /^remote:/.test(activeTab.id || '')) {
|
||||||
|
activeEnvironmentId = activeTab.id.replace(/^remote:/, '');
|
||||||
|
}
|
||||||
|
var activeRefreshable = (state.activeTarget && (state.activeTarget.kind === 'remote' || state.activeTarget.kind === 'local')) ||
|
||||||
|
(activeTab && activeTab.id !== 'home');
|
||||||
|
var envActions = activeEnvironmentId ? '<button class="btn sm tb-action no-drag" data-cc-action="env-row-menu" data-cc-environment-id="' + esc(activeEnvironmentId) + '" title="Open environment actions">Open environment in...</button>' : '';
|
||||||
|
var refreshAction = activeRefreshable ? '<button class="icon-btn tb-action no-drag" data-cc-action="refresh-tab" title="Refresh tab">' + icon('refresh', 16) + '</button>' : '';
|
||||||
|
var logoutAction = (conn || authState(state) === 'expired') ? '<button class="icon-btn tb-action no-drag" data-cc-action="logout" title="Logout">' + icon('logOut', 16) + '</button>' : '';
|
||||||
return '<div class="titlebar">' +
|
return '<div class="titlebar">' +
|
||||||
'<div class="brand"><img class="mk" src="' + esc(LOGO_URL) + '" alt=""><span>CloudCLI</span></div>' +
|
'<div class="brand"><img class="mk" src="' + esc(LOGO_URL) + '" alt=""><span>CloudCLI</span></div>' +
|
||||||
'<div class="tb-tabs no-drag">' + renderTabs(state) + '</div>' +
|
'<div class="tb-tabs no-drag">' + renderTabs(state) + '</div>' +
|
||||||
'<span style="flex:1"></span>' +
|
'<span style="flex:1"></span>' +
|
||||||
|
refreshAction +
|
||||||
envActions +
|
envActions +
|
||||||
'<button class="btn sm tb-action no-drag" data-cc-action="connect" title="' + esc(authState(state) === 'expired' ? 'Reconnect your CloudCLI account' : accountLabel(state)) + '"><span class="dot" style="background:' + (conn ? 'var(--ok)' : (authState(state) === 'expired' ? 'var(--warn)' : 'var(--tx3)')) + '"></span>' + esc(accountLabel(state)) + '</button>' +
|
'<button class="btn sm tb-action no-drag" data-cc-action="connect" title="' + esc(authState(state) === 'expired' ? 'Reconnect your CloudCLI account' : accountLabel(state)) + '"><span class="dot" style="background:' + (conn ? 'var(--ok)' : (authState(state) === 'expired' ? 'var(--warn)' : 'var(--tx3)')) + '"></span>' + esc(accountLabel(state)) + '</button>' +
|
||||||
|
logoutAction +
|
||||||
'<button class="icon-btn tb-action no-drag" data-cc-action="settings-toggle" title="Settings">' + icon('settings', 16) + '</button>' +
|
'<button class="icon-btn tb-action no-drag" data-cc-action="settings-toggle" title="Settings">' + icon('settings', 16) + '</button>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async function requestComputerUsePermission(permission) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openExternalUrl(url) {
|
async function openExternalUrl(url) {
|
||||||
if (String(url).startsWith(`${CALLBACK_PROTOCOL}://`)) {
|
if (String(url).startsWith(CALLBACK_PROTOCOL + "://")) {
|
||||||
await handleDeepLink(url);
|
await handleDeepLink(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -251,9 +251,11 @@ function getEnvironmentTarget(environment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getEnvironmentLaunchTarget(environment) {
|
async function getEnvironmentLaunchTarget(environment) {
|
||||||
|
const environmentUrl = cloud.getEnvironmentUrl(environment);
|
||||||
return {
|
return {
|
||||||
...getEnvironmentTarget(environment),
|
...getEnvironmentTarget(environment),
|
||||||
url: await cloud.getEnvironmentLaunchUrl(environment),
|
url: environmentUrl,
|
||||||
|
loadUrl: await cloud.getEnvironmentLaunchUrl(environment),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,6 +683,15 @@ async function openEnvironmentInDesktop(environment) {
|
|||||||
|
|
||||||
async function clearCloudAccount() {
|
async function clearCloudAccount() {
|
||||||
await cloud.clearCloudAccount();
|
await cloud.clearCloudAccount();
|
||||||
|
const removedTabs = tabs.removeByKind('remote');
|
||||||
|
for (const tab of removedTabs) {
|
||||||
|
desktopWindow?.destroyTabView(tab.id);
|
||||||
|
}
|
||||||
|
if (activeTarget?.kind === 'remote') {
|
||||||
|
await desktopWindow?.showLauncher();
|
||||||
|
} else {
|
||||||
|
syncDesktopState();
|
||||||
|
}
|
||||||
return getDesktopState();
|
return getDesktopState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,6 +751,8 @@ function registerIpcHandlers() {
|
|||||||
await refreshCloudEnvironments({ showErrors: true });
|
await refreshCloudEnvironments({ showErrors: true });
|
||||||
return getDesktopState();
|
return getDesktopState();
|
||||||
});
|
});
|
||||||
|
ipcMain.handle('cloudcli-desktop:disconnect-cloud', async () => clearCloudAccount());
|
||||||
|
ipcMain.handle('cloudcli-desktop:reload-active-tab', async () => desktopWindow.reloadActiveTab());
|
||||||
ipcMain.handle('cloudcli-desktop:show-environment-picker', async () => showEnvironmentPicker());
|
ipcMain.handle('cloudcli-desktop:show-environment-picker', async () => showEnvironmentPicker());
|
||||||
ipcMain.handle('cloudcli-desktop:show-launcher', async () => {
|
ipcMain.handle('cloudcli-desktop:show-launcher', async () => {
|
||||||
await desktopWindow.showLauncher();
|
await desktopWindow.showLauncher();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
|||||||
if (window.location.protocol === 'file:') {
|
if (window.location.protocol === 'file:') {
|
||||||
contextBridge.exposeInMainWorld('cloudcliDesktop', {
|
contextBridge.exposeInMainWorld('cloudcliDesktop', {
|
||||||
connectCloud: () => ipcRenderer.invoke('cloudcli-desktop:connect-cloud'),
|
connectCloud: () => ipcRenderer.invoke('cloudcli-desktop:connect-cloud'),
|
||||||
|
disconnectCloud: () => ipcRenderer.invoke('cloudcli-desktop:disconnect-cloud'),
|
||||||
copyDiagnostics: () => ipcRenderer.invoke('cloudcli-desktop:copy-diagnostics'),
|
copyDiagnostics: () => ipcRenderer.invoke('cloudcli-desktop:copy-diagnostics'),
|
||||||
copyLocalWebUrl: () => ipcRenderer.invoke('cloudcli-desktop:copy-local-web-url'),
|
copyLocalWebUrl: () => ipcRenderer.invoke('cloudcli-desktop:copy-local-web-url'),
|
||||||
getState: () => ipcRenderer.invoke('cloudcli-desktop:get-state'),
|
getState: () => ipcRenderer.invoke('cloudcli-desktop:get-state'),
|
||||||
@@ -12,6 +13,7 @@ if (window.location.protocol === 'file:') {
|
|||||||
openLocal: () => ipcRenderer.invoke('cloudcli-desktop:open-local'),
|
openLocal: () => ipcRenderer.invoke('cloudcli-desktop:open-local'),
|
||||||
openLocalWebUi: () => ipcRenderer.invoke('cloudcli-desktop:open-local-web-ui'),
|
openLocalWebUi: () => ipcRenderer.invoke('cloudcli-desktop:open-local-web-ui'),
|
||||||
refreshEnvironments: () => ipcRenderer.invoke('cloudcli-desktop:refresh-environments'),
|
refreshEnvironments: () => ipcRenderer.invoke('cloudcli-desktop:refresh-environments'),
|
||||||
|
refreshActiveTab: () => ipcRenderer.invoke('cloudcli-desktop:reload-active-tab'),
|
||||||
showEnvironmentPicker: () => ipcRenderer.invoke('cloudcli-desktop:show-environment-picker'),
|
showEnvironmentPicker: () => ipcRenderer.invoke('cloudcli-desktop:show-environment-picker'),
|
||||||
showLauncher: () => ipcRenderer.invoke('cloudcli-desktop:show-launcher'),
|
showLauncher: () => ipcRenderer.invoke('cloudcli-desktop:show-launcher'),
|
||||||
showComputerAccess: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-access'),
|
showComputerAccess: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-access'),
|
||||||
|
|||||||
@@ -55,6 +55,22 @@ export class TabsController {
|
|||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeByKind(kind) {
|
||||||
|
const removed = this.tabs.filter((tab) => tab.kind === kind && tab.closable);
|
||||||
|
if (!removed.length) return [];
|
||||||
|
|
||||||
|
const removedIds = new Set(removed.map((tab) => tab.id));
|
||||||
|
this.tabs = this.tabs.filter((tab) => !removedIds.has(tab.id));
|
||||||
|
if (removedIds.has(this.activeTabId)) {
|
||||||
|
this.activeTabId = 'home';
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveTab() {
|
||||||
|
return this.getTab(this.activeTabId);
|
||||||
|
}
|
||||||
|
|
||||||
getTab(tabId) {
|
getTab(tabId) {
|
||||||
return this.tabs.find((item) => item.id === tabId) || null;
|
return this.tabs.find((item) => item.id === tabId) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,71 @@ export class ViewHost {
|
|||||||
this.activeContentView = null;
|
this.activeContentView = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detachActiveView() {
|
||||||
|
const mainWindow = this.getMainWindow();
|
||||||
|
const view = this.activeContentView;
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed() || !view) return false;
|
||||||
|
try {
|
||||||
|
if (mainWindow.getBrowserViews().includes(view)) {
|
||||||
|
mainWindow.removeBrowserView(view);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.activeContentView = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveView() {
|
||||||
|
const view = this.activeContentView;
|
||||||
|
if (!view || view.webContents.isDestroyed()) return null;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
openActiveViewDevTools() {
|
||||||
|
const view = this.getActiveView();
|
||||||
|
if (!view) return false;
|
||||||
|
view.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadActiveView() {
|
||||||
|
const view = this.getActiveView();
|
||||||
|
if (!view) return false;
|
||||||
|
view.webContents.reloadIgnoringCache();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTabViewDiagnostics() {
|
||||||
|
const mainWindow = this.getMainWindow();
|
||||||
|
const attachedViews = new Set();
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
for (const view of mainWindow.getBrowserViews()) {
|
||||||
|
attachedViews.add(view);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore teardown races while gathering best-effort diagnostics.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.tabViews.entries()).map(([tabId, view]) => {
|
||||||
|
const { webContents } = view;
|
||||||
|
const destroyed = webContents.isDestroyed();
|
||||||
|
return {
|
||||||
|
tabId,
|
||||||
|
webContentsId: destroyed ? null : webContents.id,
|
||||||
|
url: destroyed ? null : webContents.getURL(),
|
||||||
|
title: destroyed ? null : webContents.getTitle(),
|
||||||
|
osProcessId: destroyed || typeof webContents.getOSProcessId !== 'function' ? null : webContents.getOSProcessId(),
|
||||||
|
processId: destroyed || typeof webContents.getProcessId !== 'function' ? null : webContents.getProcessId(),
|
||||||
|
attached: attachedViews.has(view),
|
||||||
|
active: this.activeContentView === view,
|
||||||
|
destroyed,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getOrCreateTabView(tabId) {
|
getOrCreateTabView(tabId) {
|
||||||
let view = this.tabViews.get(tabId);
|
let view = this.tabViews.get(tabId);
|
||||||
if (view) return view;
|
if (view) return view;
|
||||||
@@ -162,25 +227,34 @@ export class ViewHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showContentTarget(tabId, target) {
|
async showContentTarget(tabId, target) {
|
||||||
if (!isHttpUrl(target.url)) {
|
const loadUrl = target.loadUrl || target.url;
|
||||||
throw new Error(`Refusing to load unsupported app URL: ${target.url}`);
|
if (!isHttpUrl(loadUrl)) {
|
||||||
|
throw new Error(`Refusing to load unsupported app URL: ${loadUrl}`);
|
||||||
}
|
}
|
||||||
const view = this.getOrCreateTabView(tabId);
|
const view = this.getOrCreateTabView(tabId);
|
||||||
this.attach(view);
|
this.attach(view);
|
||||||
if (view.__cloudcliLoadedUrl !== target.url) {
|
if (view.__cloudcliLoadedUrl !== target.url) {
|
||||||
view.__cloudcliLoadingUrl = target.url;
|
view.__cloudcliLoadingUrl = loadUrl;
|
||||||
try {
|
try {
|
||||||
await loadUrlWithTimeout(view.webContents, target.url);
|
await loadUrlWithTimeout(view.webContents, loadUrl);
|
||||||
view.__cloudcliLoadedUrl = target.url;
|
view.__cloudcliLoadedUrl = target.url;
|
||||||
view.__cloudcliStartupHtml = null;
|
view.__cloudcliStartupHtml = null;
|
||||||
|
delete target.loadUrl;
|
||||||
} finally {
|
} finally {
|
||||||
if (view.__cloudcliLoadingUrl === target.url) {
|
if (view.__cloudcliLoadingUrl === loadUrl) {
|
||||||
view.__cloudcliLoadingUrl = null;
|
view.__cloudcliLoadingUrl = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadTab(tabId) {
|
||||||
|
const view = this.tabViews.get(tabId);
|
||||||
|
if (!view || view.webContents.isDestroyed()) return false;
|
||||||
|
view.webContents.reloadIgnoringCache();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
destroyTabView(tabId) {
|
destroyTabView(tabId) {
|
||||||
const view = this.tabViews.get(tabId);
|
const view = this.tabViews.get(tabId);
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user