Frame Editor Menus, Toolbar, and Icon Pinning
This document describes the command/menu system in CoCalc's frame editor — how menus are built, how the symbol bar (quick-access toolbar) works, and how users pin/unpin icons from menus to the toolbar.
Companion docs: frame-editors.md (layout tree, editor specs), frame-editor-dnd.md (drag-and-drop).
Overview
Each menu item has an icon on its left. Clicking the icon toggles whether that command is pinned to the symbol bar. Clicking the label executes the command. Pinned icons appear highlighted (background: #ddd) in the menu.
Architecture
Registration: Commands, Groups, and Menus
Hierarchy
MENUS(menus.ts): top-level dropdown menus. Each has alabel,pos(sort order), andgroups[]listing which groups it contains.GROUPS(menus.ts): maps group name → array of command names.COMMANDS(commands.ts): maps command name →Commandobject.
Registration flow
Editor-specific registration
Each editor type can add its own menus and commands via addEditorMenus() helper (in editor-menus.ts). This builds both menu groups and command objects from a simpler specification format, supporting nested children for submenus.
Example: format-commands.tsx registers format commands with submenu children (font size, font family, headers, colors, etc.).
The Command Interface
Icon types
Icons can be one of three forms:
String (
IconName): e.g."bold","undo","colors"— renders via<Icon name={...} />ReactNode: e.g.
<span style={{fontSize: 18}}>A</span>— custom renderingFunction:
(opts: ManageCommands) => ReactNode— dynamic icon based on state
ManageCommands Class (manage.tsx)
Central class instantiated per frame title bar. Handles:
Visibility
Checks (in order):
Explicitly hidden by spec (
spec.commands["-commandName"]) → falsealwaysShow→ trueneverVisibleOnMobile+ mobile → falseStudent project restrictions → false
Custom
isVisiblepredicateMust be in
spec.commandsorspec.customizeCommands
Menu rendering
Converts a Command into an antd MenuItem. When button=true, renders in toolbar style (icon + optional small label). When button=false, renders in menu style (icon + full label + keyboard shortcut).
Icon pinning
Editor key: the frame editor name (e.g. cm, jupyter, slate). The per-user DKV key for toolbar icons is icons-${editorName}.
Storage
Pin state and ordering are stored together in the user's per-account Conat DKV named frame-editor-settings:
missing key = fall back to editor spec defaults
empty array = explicitly show no toolbar buttons for that editor
array order = visible toolbar order
array membership = pinned state
Toolbar management commands
Remove All:
removeAllToolbarButtons()— stores an empty array for that editorReset:
resetToolbar()— deletes the DKV key, restoring editor defaults
Title Bar Rendering (title-bar.tsx)
Menu rendering
Items within groups are sorted by pos. Groups are separated by dividers.
Symbol bar rendering
Each toolbar button:
If the command has
children→ renders as aDropdownMenu(click opens submenu)Otherwise → renders as a simple
Buttonwith icon + tooltip
Symbol bar labels
Users can toggle labels below icons via right-click context menu on the symbol bar. Stored in account.other_settings.show_symbol_bar_labels.
When labels are shown:
Symbol bar moves to its own row below the menu bar
Each button shows a small label (11px) below the icon, truncated to 50px
When labels are hidden:
Symbol bar is inline with the menu bar (more compact)
Pin toggle UI in menus
When extra_button_bar is enabled (the toolbar is visible), each top-level menu item's icon becomes a clickable button:
The icon shows a tooltip: "Click icon to add/remove from toolbar". Only top-level commands (not submenu children) have this toggle.
Default Pinned Buttons per Editor
Each EditorDescription can declare default toolbar buttons:
These appear on the toolbar by default until the user explicitly unpins them.
Examples of editor defaults
Jupyter: insert cell, run cell, interrupt kernel, restart kernel, cell type
LaTeX: AI formula, sync, header, text, font, color, build, build-on-save, ToC
Markdown/Slate: AI formula, sync, header, text, font, color, ToC
Code: (minimal — typically just undo/redo from generic commands)
Custom Layout Save/Load
Users can save and restore their preferred frame layout per file type via two commands in the app menu (frame_types group):
Save Layout (
save_custom_layout) — strips transient fields (IDs, font sizes, etc.) from the current frame tree and persists the structural layout to the user's Conat DKV.Load Custom Layout (
load_custom_layout) — reads the saved layout back from the DKV and replaces the current frame tree.
Both commands have alwaysShow: true so they appear for every editor type.
Storage
Layouts are stored in the same frame-editor-settings DKV used for toolbar button persistence. The key format is custom-layout-{ext} where {ext} is the file extension (e.g. py, ipynb, tex). This means a saved layout applies to all files of the same type.
Implementation
stripFrameTreeIds() uses a whitelist of structural fields (type, direction, pos, sizes, activeTab, first, second, children) to keep only the layout structure. isFrameTree() is a runtime type guard that validates DKV data before applying it, preventing crashes from corrupted data.
File Menu → "+New" Action
The File → "+ New" command (generic-commands.tsx, group new-open) opens the +New flyout panel and pre-fills the filename extension from the current file. It uses fileAction("new") which calls ProjectActions.show_file_action_panel({ path, action: "new", source: "editor" }).
When source === "editor", the action also sets flyout_new_path to the parent directory of the file being edited, so the new file is created next to the current one. Only the flyout path is changed (since the flyout is what opens); the full-page new_page_path is left untouched. See project-files.md §"+New" Path Model for the full path model.
Other entry points to the File menu (rename, copy, move, delete, etc.) use fileAction(action) which routes through the same show_file_action_panel but opens the standard file-action modal instead.