Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import { app, BrowserWindow, Menu } from 'electron';
|
|
|
|
function sendMenuAction(action: string): void {
|
|
const win = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
|
|
win?.webContents.send('menu:action', action);
|
|
}
|
|
|
|
/** Recent files submenu — rebuilt each time the menu opens. */
|
|
function buildRecentFilesSubmenu(): Electron.MenuItemConstructorOptions[] {
|
|
const recent = app.isReady() ? ((global as any).__recentFiles ?? []) : [];
|
|
if (recent.length === 0) {
|
|
return [{ label: 'No Recent Files', enabled: false }];
|
|
}
|
|
const items: Electron.MenuItemConstructorOptions[] = recent.map(
|
|
(entry: { fileName: string; filePath: string }) => ({
|
|
label: entry.fileName,
|
|
click: () => sendMenuAction(`open-recent:${entry.filePath}`),
|
|
}),
|
|
);
|
|
items.push({ type: 'separator' });
|
|
items.push({
|
|
label: 'Clear Recent Files',
|
|
click: () => sendMenuAction('clear-recent-files'),
|
|
});
|
|
return items;
|
|
}
|
|
|
|
export function buildAppMenu(): void {
|
|
const isMac = process.platform === 'darwin';
|
|
|
|
const template: Electron.MenuItemConstructorOptions[] = [
|
|
// macOS app menu
|
|
...(isMac
|
|
? [
|
|
{
|
|
label: app.name,
|
|
submenu: [
|
|
{ role: 'about' as const },
|
|
{ type: 'separator' as const },
|
|
{ role: 'services' as const },
|
|
{ type: 'separator' as const },
|
|
{ role: 'hide' as const },
|
|
{ role: 'hideOthers' as const },
|
|
{ role: 'unhide' as const },
|
|
{ type: 'separator' as const },
|
|
{ role: 'quit' as const },
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
|
|
// File menu
|
|
{
|
|
label: 'File',
|
|
submenu: [
|
|
{
|
|
label: 'New',
|
|
accelerator: 'CmdOrCtrl+N',
|
|
click: () => sendMenuAction('new'),
|
|
},
|
|
{
|
|
label: 'Open\u2026',
|
|
accelerator: 'CmdOrCtrl+O',
|
|
click: () => sendMenuAction('open'),
|
|
},
|
|
{
|
|
label: 'Open Recent',
|
|
submenu: buildRecentFilesSubmenu(),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: 'Save',
|
|
accelerator: 'CmdOrCtrl+S',
|
|
click: () => sendMenuAction('save'),
|
|
},
|
|
{
|
|
label: 'Save As\u2026',
|
|
accelerator: 'CmdOrCtrl+Shift+S',
|
|
click: () => sendMenuAction('save-as'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: 'Export Image\u2026',
|
|
// Use Cmd+Shift+P (P = Print/PDF/Picture). Cmd+Shift+E was being
|
|
// swallowed at the OS level by Chinese IMEs / system tools on
|
|
// macOS before reaching the renderer.
|
|
//
|
|
// `registerAccelerator: false` keeps the hint visible in the menu
|
|
// but tells Electron NOT to register it with the OS — the
|
|
// keystroke is handled by the renderer's capture-phase document
|
|
// keydown listener in editor-layout.tsx, which avoids HMR/IPC
|
|
// fragility.
|
|
accelerator: 'CmdOrCtrl+Shift+P',
|
|
registerAccelerator: false,
|
|
click: () => sendMenuAction('export-image'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: 'Import Figma\u2026',
|
|
accelerator: 'CmdOrCtrl+Shift+F',
|
|
click: () => sendMenuAction('import-figma'),
|
|
},
|
|
...(!isMac ? [{ type: 'separator' as const }, { role: 'quit' as const }] : []),
|
|
],
|
|
},
|
|
|
|
// Edit menu (role-based for native text input support)
|
|
{
|
|
label: 'Edit',
|
|
submenu: [
|
|
{
|
|
label: 'Undo',
|
|
accelerator: 'CmdOrCtrl+Z',
|
|
click: () => sendMenuAction('undo'),
|
|
},
|
|
{
|
|
label: 'Redo',
|
|
accelerator: 'CmdOrCtrl+Shift+Z',
|
|
click: () => sendMenuAction('redo'),
|
|
},
|
|
{ type: 'separator' },
|
|
{ role: 'cut' },
|
|
{ role: 'copy' },
|
|
{ role: 'paste' },
|
|
...(isMac ? [{ role: 'pasteAndMatchStyle' as const }] : []),
|
|
{ role: 'selectAll' },
|
|
],
|
|
},
|
|
|
|
// View menu
|
|
{
|
|
label: 'View',
|
|
submenu: [
|
|
// Reload / Force Reload / DevTools are dev-only — hide in packaged builds.
|
|
...(app.isPackaged
|
|
? []
|
|
: [
|
|
{ role: 'reload' as const },
|
|
{ role: 'forceReload' as const },
|
|
{ role: 'toggleDevTools' as const },
|
|
{ type: 'separator' as const },
|
|
]),
|
|
{ role: 'resetZoom' },
|
|
{ role: 'zoomIn' },
|
|
{ role: 'zoomOut' },
|
|
{ type: 'separator' },
|
|
{ role: 'togglefullscreen' },
|
|
],
|
|
},
|
|
|
|
// Window menu
|
|
{
|
|
label: 'Window',
|
|
submenu: [
|
|
{ role: 'minimize' },
|
|
{ role: 'zoom' },
|
|
...(isMac
|
|
? [{ type: 'separator' as const }, { role: 'front' as const }]
|
|
: [{ role: 'close' as const }]),
|
|
],
|
|
},
|
|
];
|
|
|
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
}
|