Per user request: the "显示控制台 / Show console" button in the top-right header and the bottom drawer it opened are no longer needed — the Runs section already renders the same log tail in-page via RunDetail, so the drawer was a duplicate surface. Deleted the entire feature instead of leaving a hidden toggle that attracts bugs. Removed files: - src/renderer/views/ConsoleView.tsx (126 LOC) Removed renderer integration points: - App.tsx: header "Show/Hide console" button, ConsoleView render, setConsoleOpen(true) call after startRun, TerminalWindow icon import - views/sections/ExecutionSections.tsx: onOpenConsole callback, setConsoleOpen subscription - components/runs/RunCenter.tsx: onOpenConsole prop (removed from forwarding chain) - components/runs/RunDetail.tsx: onOpenConsole prop + the Lightning "Open console" ActionButton. The cancel-run button remains as the sole Panel action now that the console entry point is gone. - hooks/useUIController.ts: consoleOpen / setConsoleOpen subscription - hooks/useDesktopEvents.ts: "toggle-console" shortcut dispatcher - state/slices/navigation.ts: consoleOpen field, setConsoleOpen, toggleConsole actions, and the two auto-open-on-navigate triggers that used to flip the drawer when jumping to a run Removed main-process integration: - main/desktop-shell-menu.ts: "Toggle Console" menu item + CmdOrCtrl+` accelerator - main/desktop-shell-window.ts: onShortcut union type narrowed - shared/types/events.ts: ShortcutEvent action union narrowed Removed i18n keys (zh + en): - app.hideConsole / app.showConsole - console.title / console.noRuns / console.close - common.openLog / common.noRunSelected / common.waitingForLog - runs.openConsole Log capture itself (logBufferRef in useCompanyController + run-log events) is unchanged — the Runs section still displays it. Nothing was moved to a "disabled but present" state; the feature is out of the tree entirely. Typecheck and lint pass.
273 lines
7.5 KiB
TypeScript
273 lines
7.5 KiB
TypeScript
import { dirname, join } from "node:path";
|
|
import {
|
|
app,
|
|
BrowserWindow,
|
|
dialog,
|
|
Menu,
|
|
nativeImage,
|
|
shell,
|
|
Tray,
|
|
} from "electron";
|
|
import type { IpcMainInvokeEvent } from "electron";
|
|
import { IPC_CHANNELS } from "@shared/contracts";
|
|
import type { DesktopResult } from "@shared/contracts";
|
|
import { ok } from "./ipc-core-common";
|
|
|
|
export interface DesktopShellMenuDependencies {
|
|
iconPath: string;
|
|
isDev: boolean;
|
|
onCheckForUpdates: () => Promise<void>;
|
|
onBackupProfile: () => Promise<void>;
|
|
onShortcut: (action: "toggle-command-palette" | "refresh") => void;
|
|
getMainWindow: () => BrowserWindow | null;
|
|
ensureWindow: () => Promise<BrowserWindow | null>;
|
|
requestQuitWithConfirmation: () => Promise<void>;
|
|
registerHandle: (
|
|
channel: string,
|
|
handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise<DesktopResult<unknown>> | DesktopResult<unknown>,
|
|
) => void;
|
|
}
|
|
|
|
export function createDesktopShellMenu(deps: DesktopShellMenuDependencies) {
|
|
const {
|
|
iconPath,
|
|
isDev,
|
|
onCheckForUpdates,
|
|
onBackupProfile,
|
|
onShortcut,
|
|
getMainWindow,
|
|
ensureWindow,
|
|
requestQuitWithConfirmation,
|
|
registerHandle,
|
|
} = deps;
|
|
|
|
let appTray: Tray | null = null;
|
|
|
|
function buildMenu() {
|
|
const macAppMenu: Electron.MenuItemConstructorOptions = {
|
|
label: app.name,
|
|
submenu: [
|
|
{ role: "about" },
|
|
{ type: "separator" },
|
|
{ role: "services" },
|
|
{ type: "separator" },
|
|
{ role: "hide" },
|
|
{ role: "hideOthers" },
|
|
{ role: "unhide" },
|
|
{ type: "separator" },
|
|
{
|
|
label: `Quit ${app.name}`,
|
|
accelerator: "CmdOrCtrl+Q",
|
|
click: () => void requestQuitWithConfirmation(),
|
|
},
|
|
],
|
|
};
|
|
const template: Electron.MenuItemConstructorOptions[] = [
|
|
...(process.platform === "darwin" ? [macAppMenu] : []),
|
|
{
|
|
label: "File",
|
|
submenu: [
|
|
{
|
|
label: "Open Workspace Folder",
|
|
accelerator: "CmdOrCtrl+O",
|
|
click: async () => {
|
|
const result = await dialog.showOpenDialog({
|
|
properties: ["openDirectory"],
|
|
});
|
|
const targetPath = result.filePaths[0];
|
|
if (result.canceled || !targetPath) return;
|
|
shell.openPath(targetPath);
|
|
},
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
label: "Backup Profile",
|
|
accelerator: "CmdOrCtrl+Shift+B",
|
|
click: async () => {
|
|
await onBackupProfile();
|
|
},
|
|
},
|
|
...(process.platform === "darwin" ? [] : [
|
|
{ type: "separator" as const },
|
|
{
|
|
label: "Quit",
|
|
accelerator: "Alt+F4",
|
|
click: () => void requestQuitWithConfirmation(),
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
{
|
|
label: "Edit",
|
|
submenu: [
|
|
{ role: "undo" },
|
|
{ role: "redo" },
|
|
{ type: "separator" },
|
|
{ role: "cut" },
|
|
{ role: "copy" },
|
|
{ role: "paste" },
|
|
{ role: "selectAll" },
|
|
],
|
|
},
|
|
{
|
|
label: "View",
|
|
submenu: [
|
|
{
|
|
label: "Command Palette",
|
|
accelerator: "CmdOrCtrl+K",
|
|
click: () => onShortcut("toggle-command-palette"),
|
|
},
|
|
{
|
|
label: "Refresh Data",
|
|
accelerator: "CmdOrCtrl+R",
|
|
click: () => onShortcut("refresh"),
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
role: "toggleDevTools",
|
|
// Hidden in packaged builds unless opted in via env var so power
|
|
// users can still inspect rendered state without a dev build.
|
|
visible: isDev || process.env.AGENTCOMPANY_ENABLE_DEVTOOLS === "true",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "Run",
|
|
submenu: [
|
|
{
|
|
label: "Check For Updates",
|
|
click: async () => {
|
|
await onCheckForUpdates();
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
const menu = Menu.buildFromTemplate(template);
|
|
const mainWindow = getMainWindow();
|
|
if (process.platform === "darwin") {
|
|
Menu.setApplicationMenu(menu);
|
|
} else {
|
|
// Frameless windows: hide native menu bar but keep keyboard accelerators active
|
|
Menu.setApplicationMenu(menu);
|
|
mainWindow?.setMenuBarVisibility(false);
|
|
mainWindow?.setAutoHideMenuBar(true);
|
|
}
|
|
}
|
|
|
|
function createTray() {
|
|
if (appTray) {
|
|
return;
|
|
}
|
|
|
|
const assetsDir = dirname(iconPath);
|
|
let trayIcon: Electron.NativeImage;
|
|
|
|
if (process.platform === "darwin") {
|
|
// macOS menu bar: use Template images (monochrome black + alpha).
|
|
// Electron auto-detects the @2x variant when the base name includes "Template".
|
|
const templatePath = join(assetsDir, "tray-iconTemplate.png");
|
|
trayIcon = nativeImage.createFromPath(templatePath);
|
|
if (!trayIcon.isEmpty()) {
|
|
trayIcon.setTemplateImage(true);
|
|
}
|
|
} else {
|
|
// Windows / Linux: use the color tray icon
|
|
const trayPath = join(assetsDir, "tray-icon.png");
|
|
trayIcon = nativeImage.createFromPath(trayPath);
|
|
}
|
|
|
|
// Fallback: resize the full app icon
|
|
if (trayIcon.isEmpty()) {
|
|
const fallback = nativeImage.createFromPath(iconPath);
|
|
if (!fallback.isEmpty()) {
|
|
trayIcon = fallback.resize({ width: 32, height: 32 });
|
|
if (process.platform === "darwin") {
|
|
trayIcon.setTemplateImage(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
appTray = new Tray(trayIcon.isEmpty() ? nativeImage.createEmpty() : trayIcon);
|
|
appTray.setToolTip("AgentCompany");
|
|
appTray.setContextMenu(
|
|
Menu.buildFromTemplate([
|
|
{
|
|
label: "Show AgentCompany",
|
|
click: () => {
|
|
void ensureWindow().then((win) => {
|
|
if (win) {
|
|
win.show();
|
|
win.focus();
|
|
}
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: "Hide AgentCompany",
|
|
click: () => {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.hide();
|
|
}
|
|
},
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
label: "Quit",
|
|
click: () => void requestQuitWithConfirmation(),
|
|
},
|
|
]),
|
|
);
|
|
appTray.on("click", () => {
|
|
void ensureWindow().then((win) => {
|
|
if (!win) return;
|
|
if (win.isVisible()) {
|
|
win.hide();
|
|
return;
|
|
}
|
|
win.show();
|
|
win.focus();
|
|
});
|
|
});
|
|
}
|
|
|
|
function registerWindowControlIpc() {
|
|
// Window control IPC for custom title bar (Windows/Linux frameless windows).
|
|
// Routed through registerHandle so the trusted-sender check blocks
|
|
// embedded webContents from closing/minimizing the app.
|
|
registerHandle(IPC_CHANNELS.windowMinimize, () => {
|
|
getMainWindow()?.minimize();
|
|
return ok(null);
|
|
});
|
|
registerHandle(IPC_CHANNELS.windowMaximize, () => {
|
|
const mainWindow = getMainWindow();
|
|
if (mainWindow?.isMaximized()) {
|
|
mainWindow.unmaximize();
|
|
} else {
|
|
mainWindow?.maximize();
|
|
}
|
|
return ok(null);
|
|
});
|
|
registerHandle(IPC_CHANNELS.windowClose, () => {
|
|
getMainWindow()?.close();
|
|
return ok(null);
|
|
});
|
|
registerHandle(IPC_CHANNELS.windowIsMaximized, () => {
|
|
return ok(getMainWindow()?.isMaximized() ?? false);
|
|
});
|
|
}
|
|
|
|
function destroy() {
|
|
appTray?.destroy();
|
|
appTray = null;
|
|
}
|
|
|
|
return {
|
|
buildMenu,
|
|
createTray,
|
|
registerWindowControlIpc,
|
|
destroy,
|
|
};
|
|
}
|