Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/UI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ This contract preserves the approved MVP concept and prevents feature ownership
- With window snapping enabled, drag and resize use a hidden `10px` grid and a `10px` magnetic threshold for neighboring edges, centers, and a consistent `20px` gap.
- The empty-canvas context menu also lists Files and a **Tools** submenu between Browser and Settings. **Tools** holds a single **Auto-sort** action whose criteria (by task, i.e. subagents to the right of the session that spawned them; by status; by project folder; grid) are radio rows in that submenu; there is no separate layouts surface, toolbar, or saved preset. Auto-sort moves the windows of the current multi-selection when it holds at least two, or else every terminal, plugin, and Browser window that is not completely inside a region (companion mascots move only when selected); sticky notes, materials, and Files cards never move. The sorted block keeps the snapping grid and `20px` gap when snapping is on, and shifts right or down until it is clear of HOME, regions, notes, materials, Files cards, and windows it does not move. The moved windows show the existing selection outline for a few seconds or until the next press; it is presentation-only and does not select them. They glide into place unless reduced motion is requested, and **Undo auto-sort** stays in the same submenu while any of them is still where the sort put it. Undo restores only those untouched windows; there is no global undo shortcut, so terminal and text inputs keep their keys. The criterion is remembered only for the running session. A region's Tools submenu sorts only the windows inside that region and keeps them there: the arrangement goes to the region's center, or to the clear place nearest it when something is in the way; if the near-square grid does not fit, the nearest column count that fits is used (the task tree keeps its shape), and the sort is refused only when no shape fits anywhere in the region.
- One trusted context dispatcher owns right-clicks on the canvas. Truly empty canvas offers region creation, a sticky note, the Settings-configured launcher submenu, Browser, and Settings. Holding RMB on empty canvas also opens the configurable radial quick launcher: moving toward an action and releasing activates it, while a stationary right-click continues to open the standard empty-canvas menu. Region and note right-clicks remain context-specific. A region offers rename, color, a note at the pressed point, and region-only deletion. A note frame offers edit, bring-to-front, and deletion. Terminal, Browser, plugin, textarea/input, and contenteditable surfaces keep their native context menus; middle-button panning is unchanged. `Cmd/Ctrl+K` opens a searchable palette over live sessions and the same global creation/launch actions. Regions persist independently of terminal restore, render behind HOME and windows, resize from every edge/corner, and move every terminal, plugin, Browser, or note whose complete bounds were inside the region at drag start. Attached contents follow the region continuously during the gesture and persist once at pointer release; deleting a region never deletes its contents. Sticky notes persist bounded text and geometry, snap like other windows, and resize from every edge/corner. General Settings exposes independent **Save colored regions after exit** and **Save notes after exit** controls. Both default on to preserve existing profiles. Turning either off removes only that collection from disk persistence without deleting its live canvas objects; turning it back on saves the current collection, while leaving it off starts the next launch with that collection empty.
- Context menus, region editors, the radial launcher, and the command palette use official Lucide/provider assets and shared application color tokens. Their dimensions follow root `--ui-scale`. Appearance owns the `0.85–1.25` scale in `0.05` steps and the sage/lilac/night system palette; the scale changes UI chrome rather than canvas zoom or terminal font size. Agents owns the independent context-menu and radial-launcher subsets and their safe reset defaults.
- Context menus, region editors, the radial launcher, and the command palette use official Lucide/provider assets and shared application color tokens. Their dimensions follow root `--ui-scale`, as do the terminal card options menu and the canvas shortcut-hints panel. Appearance owns the `0.85–1.25` scale in `0.05` steps and the sage/lilac/night system palette; the scale changes UI chrome rather than canvas zoom or terminal font size. Agents owns the independent context-menu and radial-launcher subsets and their safe reset defaults.
- Settings uses the approved two-pane modal grammar: a persistent icon-labelled sidebar for all seven sections, a separate current-section header, and a scrollable content pane. Settings must not fall back to the legacy light slide-over with one horizontal segmented tab strip. Simple settings use compact copy/control rows; all Settings chrome reads the active system tokens, official Lucide assets, and root `--ui-scale`.
- General Settings places the macOS/Windows/Linux/Custom keyboard preset after language, followed by an expandable Keyboard shortcuts section for canvas, terminal, Codex editor and canvas navigation bindings. Recording a shortcut selects Custom; choosing a named preset replaces bindings. Optional commands can be disabled, while submit, newline and whole-draft selection retain recorded bindings. Submit/newline bindings apply to new or restarted ordinary and bundled Codex cards; whole-draft selection requires the compatible bundled frontend and is unavailable in stock Codex. Canvas and terminal bindings update immediately. Existing cards retain their launch snapshot. Stock Codex hints may show the transport keys Enter/Shift+Enter; personal submit/newline bindings are overridden for the launch without changing the personal file. Focused terminal and text inputs retain ownership of canvas keyboard shortcuts on every platform.
- Empty-canvas drag and middle-button drag pan the canvas. On fresh profiles ordinary scroll over empty canvas pans on both axes; pinch and `Cmd/Ctrl + scroll` zoom around the pointer. **Use scroll wheel to zoom** restores historical ordinary-wheel zoom and is enabled by migration for existing profiles. Only a focused input widget interrupts canvas wheel/pinch, except the HOME session list keeps ordinary local scrolling without a focus precondition; unfocused or non-focusable widgets otherwise leave those events to the canvas. **Wheel/pinch capture over widgets** has Off, On, and Key modes: Off preserves local ownership, On always transfers only wheel/pinch to the canvas, and Key transfers only wheel/pinch while its binding is held. Key and full-navigation bindings accept modifier chords or Mouse3/Mouse4/Mouse5. A focused live Browser page therefore scrolls normally in Off or released-Key mode; unfocused Browser content, On, an active Key binding, the full override, pinch, and `Cmd/Ctrl + scroll` are canvas-owned. The owner selected for a Browser wheel sequence is held until 250 ms of wheel inactivity. Browser summaries and placeholders are always canvas-owned. Fresh profiles use Key with `Command` on macOS or `Ctrl` elsewhere. A separate configurable **Canvas navigation override** (default `Option` on macOS and `Alt` elsewhere) transfers wheel/pinch and drag while held. While full override is held all canvas surfaces show `grab`; an owned drag shows `grabbing` until pointer up/cancel even after the binding is released.
Expand Down
8 changes: 3 additions & 5 deletions src/renderer/src/components/ShortcutReference.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
import type { AppSettings } from "../../../shared/contracts";
import { displayCanvasNavigationBinding } from "../lib/shortcuts";
import { displayCanvasNavigationBinding, displayShortcutBinding } from "../lib/shortcuts";
import { UiIcon } from "./UiIcon";
import { t, type TranslationKey } from "../lib/i18n";

Expand All @@ -11,9 +11,7 @@ export function ShortcutReference({ settings, onClose }: {
const dialog = useRef<HTMLDialogElement>(null);
const { locale, shortcuts: bindings } = settings;
const mac = window.canvasTTY.window.isMacOS;
const display = (binding: string) => binding.replace("Meta", mac ? "Command" : "Super")
.replace("Comma", ",").replace(/ArrowUp/g, "↑").replace(/ArrowDown/g, "↓")
.replace(/ArrowLeft/g, "←").replace(/ArrowRight/g, "→") || t(locale, "disabled");
const display = (binding: string) => displayShortcutBinding(binding, mac) || t(locale, "disabled");
const groups: { title: TranslationKey; shortcuts: [string[], TranslationKey][] }[] = [
{ title: "keyboardCanvas", shortcuts: [
[[bindings.home], "homeShortcut"],
Expand All @@ -24,7 +22,7 @@ export function ShortcutReference({ settings, onClose }: {
[[bindings.toggleDetail], "keyboardToggleDetail"],
[[bindings.momEntered], "keyboardHideBackgrounds"],
[[bindings.disableDesign], "keyboardDisableDesign"],
[["Shift + drag"], "marqueeSelectionHint"]
[[t(locale, "shortcutShiftDrag")], "marqueeSelectionHint"]
] },
{ title: "keyboardNavigation", shortcuts: [
[[bindings.focusUp], "keyboardFocusUp"],
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/features/settings/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ export function SettingsPanel({
))}
<SettingGroup label={t(locale, "canvasWheelCapture")} description={t(locale, "canvasWheelCaptureDescription")}>
<Segmented value={settings.canvasWheelCaptureMode}
options={[["off", "Off"], ["always", "On"], ["key", "Key"]]}
options={[["off", t(locale, "off")], ["always", t(locale, "on")], ["key", t(locale, "canvasWheelCaptureKey")]]}
onChange={(value) => changeCanvasWheelCaptureMode(value as CanvasWheelCaptureMode)} />
{settings.canvasWheelCaptureMode === "key" && (
<CanvasNavigationShortcutEditor open={open} locale={locale}
Expand Down
14 changes: 7 additions & 7 deletions src/renderer/src/features/workspace/WorkspaceCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
} from "../../../../shared/contracts";
import { UiIcon } from "../../components/UiIcon";
import { t } from "../../lib/i18n";
import { displayCanvasNavigationBinding, isRenameInputTarget, isShortcutCaptureTarget, matchesPhysicalOrLayoutKey, matchesShortcut, shouldKeepNativeKeyboardInput } from "../../lib/shortcuts";
import { displayCanvasNavigationBinding, displayShortcutBinding, isRenameInputTarget, isShortcutCaptureTarget, matchesPhysicalOrLayoutKey, matchesShortcut, shouldKeepNativeKeyboardInput } from "../../lib/shortcuts";
import { BrowserCard } from "../browser/BrowserCard";
import type { FileBrowserCardProps, FileSessionOption } from "../files/FileBrowserCard";
import { fileCardRuntimeFor, type FileCardRuntimeState } from "../files/fileCardRuntime";
Expand Down Expand Up @@ -1738,11 +1738,11 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element
)}
{settings.showShortcutHints && settings.shortcutHintsPlacement === placement && (
<aside className="shortcut-hints" aria-label={t(settings.locale, "keyboardShortcuts")}>
<div><kbd>{settings.shortcuts.home}</kbd><span>{t(settings.locale, "homeShortcut")}</span></div>
<div><kbd>{settings.shortcuts.renameWindow}</kbd><span>{t(settings.locale, "renameWindow")}</span></div>
<div><kbd>{settings.shortcuts.toggleFullscreen.replace("Meta", window.canvasTTY.window.isMacOS ? "Command" : "Super")}</kbd><span>{t(settings.locale, "toggleFullscreen")}</span></div>
<div><kbd>{settings.shortcuts.focusUp}</kbd><span>{t(settings.locale, "keyboardFocusUp")}</span></div>
<div><kbd>Shift + drag</kbd><span>{t(settings.locale, "marqueeSelectionHint")}</span></div>
{([["home", "homeShortcut"], ["renameWindow", "renameWindow"], ["toggleFullscreen", "toggleFullscreen"],
["focusUp", "keyboardFocusUp"]] as const).map(([action, label]) => (
<div key={action}><kbd>{displayShortcutBinding(settings.shortcuts[action], window.canvasTTY.window.isMacOS)}</kbd><span>{t(settings.locale, label)}</span></div>
))}
<div><kbd>{t(settings.locale, "shortcutShiftDrag")}</kbd><span>{t(settings.locale, "marqueeSelectionHint")}</span></div>
{settings.canvasWheelCaptureMode === "key" && settings.canvasWheelOverride !== null && (
<div><kbd>{displayCanvasNavigationBinding(settings.canvasWheelOverride, window.canvasTTY.window.isMacOS)}</kbd>
<span>{t(settings.locale, "canvasWheelOverrideHint")}</span></div>
Expand All @@ -1754,7 +1754,7 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element
<button className="shortcut-hints__more" type="button" data-interactive="true"
aria-haspopup="dialog" onClick={props.onOpenShortcutReference}>
{t(settings.locale, "keyboardShortcuts")}
<UiIcon name="app-window" size={14} />
<UiIcon name="app-window" size="1.27em" />
</button>
</aside>
)}
Expand Down
20 changes: 12 additions & 8 deletions src/renderer/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,16 +522,17 @@ const ru = {
zoomSensitivity: "Чувствительность зума",
useScrollWheelToZoom: "Зум обычным скроллом",
useScrollWheelToZoomDescription: "Если выключено, обычный скролл перемещает канвас, а Ctrl/Command + скролл и pinch масштабируют.",
canvasWheelCapture: "Перехват wheel/pinch над виджетами",
canvasWheelCaptureDescription: "Off оставляет обычный wheel/pinch focused input-виджету, включая live Browser page. On и активный бинд передают события канвасу; pinch и Ctrl/Command + скролл над Browser всегда масштабируют canvas. Клики и drag не перехватываются.",
canvasWheelOverride: "Комбинация wheel/pinch",
canvasWheelOverrideHint: "Wheel/pinch поверх виджетов",
canvasWheelCapture: "Перехват колеса и жеста масштаба над виджетами",
canvasWheelCaptureDescription: "«Выключено» оставляет обычную прокрутку колесом и жест масштаба виджету с фокусом ввода, включая открытую страницу браузера. «Включено», а также «По комбинации» при нажатой комбинации, передают их канвасу; жест масштаба и Ctrl/Command + прокрутка над браузером всегда масштабируют канвас. Клики и перетаскивание не перехватываются.",
canvasWheelCaptureKey: "По комбинации",
canvasWheelOverride: "Комбинация для колеса и жеста масштаба",
canvasWheelOverrideHint: "Колесо и жест масштаба поверх виджетов",
canvasOverrideShortcutInvalid: "Нужен модификатор, комбинация с ним или Mouse3/Mouse4/Mouse5.",
canvasNavigationOverride: "Перехват навигации канвасом",
canvasNavigationOverrideDescription: "Пока комбинация или кнопка мыши удерживается, wheel, pinch и drag над любым виджетом принадлежат канвасу. Drag колёсиком работает всегда.",
canvasNavigationOverrideDescription: "Пока комбинация или кнопка мыши удерживается, прокрутка колесом, жест масштаба и перетаскивание над любым виджетом принадлежат канвасу. Перетаскивание с нажатым колёсиком работает всегда.",
canvasNavigationCaptureHint: "Удержите модификатор, нажмите комбинацию или Mouse3/Mouse4/Mouse5. Escape отменяет запись.",
canvasNavigationOverrideHint: "Навигация поверх виджетов",
canvasOverrideBindingsMatch: "Комбинации совпадают: полный перехват имеет приоритет и также забирает drag.",
canvasOverrideBindingsMatch: "Комбинации совпадают: полный перехват имеет приоритет и также забирает перетаскивание.",
disabled: "Отключено",
terminalWheelDirection: "Направление колеса терминала",
copyOnSelect: "Копировать при выделении",
Expand Down Expand Up @@ -809,7 +810,8 @@ const ru = {
terminalSearchClose: "Закрыть поиск",
fitCanvas: "Показать всё",
focusWindowHint: "Фокус на соседнее окно",
marqueeSelectionHint: "Shift + тянуть — выделить группу",
marqueeSelectionHint: "Выделить группу",
shortcutShiftDrag: "Shift + перетаскивание",
remarkAnchorStep: "Замечание к шагу",
remarkAnchorPage: "Замечание к странице",
remarkAnchorTime: "Замечание ко времени",
Expand Down Expand Up @@ -1595,6 +1597,7 @@ const en: Record<keyof typeof ru, string> = {
useScrollWheelToZoomDescription: "When off, plain scrolling pans the canvas while Ctrl/Command + scroll and pinch zoom.",
canvasWheelCapture: "Wheel/pinch capture over widgets",
canvasWheelCaptureDescription: "Off leaves plain wheel/pinch with the focused input widget, including a live Browser page. On and an active binding route it to the canvas; Browser pinch and Ctrl/Command + scroll always zoom the canvas. Clicks and drag are not captured.",
canvasWheelCaptureKey: "Key",
canvasWheelOverride: "Wheel/pinch shortcut",
canvasWheelOverrideHint: "Wheel/pinch over widgets",
canvasOverrideShortcutInvalid: "Use a modifier, a modifier-based chord, or Mouse3/Mouse4/Mouse5.",
Expand Down Expand Up @@ -1880,7 +1883,8 @@ const en: Record<keyof typeof ru, string> = {
terminalSearchClose: "Close search",
fitCanvas: "Fit to content",
focusWindowHint: "Focus the neighbouring window",
marqueeSelectionHint: "Shift + drag selects a group",
marqueeSelectionHint: "Select a group",
shortcutShiftDrag: "Shift + drag",
remarkAnchorStep: "Remark on step",
remarkAnchorPage: "Remark on page",
remarkAnchorTime: "Remark on the time",
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/src/lib/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ export function handleMacNativeSelectAll(event: KeyboardEvent, isMacOS: boolean)
return true;
}

/**
* An action shortcut as the hints panel and the shortcut reference show it: arrows as glyphs, Meta as Command or
* Super. Key by key, so the Comma key is not looked for inside "Command".
*/
export function displayShortcutBinding(binding: string, isMacOS: boolean): string {
const names: Record<string, string> = { Meta: isMacOS ? "Command" : "Super", Comma: ",", ArrowUp: "↑", ArrowDown: "↓", ArrowLeft: "←", ArrowRight: "→" };
return binding.split("+").map((key) => names[key] ?? key).join("+");
}

export function displayCanvasNavigationBinding(binding: string, isMacOS: boolean): string {
if (!isMacOS) return binding;
return binding.split("+").map((part) => {
Expand Down
Loading
Loading