Files
AgentCompany/src/main/desktop-shell-menu.ts
T
ZacharyZhang-NY fd6471988a refactor(app): remove Console feature entirely
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.
2026-04-17 20:32:34 -04:00

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,
};
}