From 91bacac739adf16e24a18e5f4f5b2855075c5ab9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 8 May 2026 15:28:17 +0200 Subject: [PATCH] fix(coding-agent): show Option key on macOS closes #4289 --- packages/coding-agent/CHANGELOG.md | 1 + .../components/keybinding-hints.ts | 32 ++++++++++++++++--- .../components/settings-selector.ts | 5 +-- .../interactive/components/tree-selector.ts | 3 +- .../src/modes/interactive/interactive-mode.ts | 23 +++---------- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 68fa535d..b4ff2d74 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)). - Fixed the interactive update notification to render the changelog as an OSC 8 hyperlink when the terminal supports hyperlinks ([#4280](https://github.com/earendil-works/pi/issues/4280)). ## [0.74.0] - 2026-05-07 diff --git a/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts b/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts index 27371bdb..bb018c18 100644 --- a/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts +++ b/packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts @@ -5,20 +5,44 @@ import { getKeybindings, type Keybinding, type KeyId } from "@earendil-works/pi-tui"; import { theme } from "../theme/theme.js"; -function formatKeys(keys: KeyId[]): string { +export interface KeyTextFormatOptions { + capitalize?: boolean; +} + +function formatKeyPart(part: string, options: KeyTextFormatOptions): string { + const displayPart = process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part; + return options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart; +} + +export function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string { + return key + .split("/") + .map((k) => + k + .split("+") + .map((part) => formatKeyPart(part, options)) + .join("+"), + ) + .join("/"); +} + +function formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string { if (keys.length === 0) return ""; - if (keys.length === 1) return keys[0]!; - return keys.join("/"); + return formatKeyText(keys.join("/"), options); } export function keyText(keybinding: Keybinding): string { return formatKeys(getKeybindings().getKeys(keybinding)); } +export function keyDisplayText(keybinding: Keybinding): string { + return formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true }); +} + export function keyHint(keybinding: Keybinding, description: string): string { return theme.fg("dim", keyText(keybinding)) + theme.fg("muted", ` ${description}`); } export function rawKeyHint(key: string, description: string): string { - return theme.fg("dim", key) + theme.fg("muted", ` ${description}`); + return theme.fg("dim", formatKeyText(key)) + theme.fg("muted", ` ${description}`); } diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 8eb1b3f1..7d9d9cdf 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -14,6 +14,7 @@ import { import type { WarningSettings } from "../../../core/settings-manager.js"; import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; +import { keyDisplayText } from "./keybinding-hints.js"; const SETTINGS_SUBMENU_SELECT_LIST_LAYOUT: SelectListLayoutOptions = { minPrimaryColumnWidth: 12, @@ -204,6 +205,7 @@ export class SettingsSelectorComponent extends Container { super(); const supportsImages = getCapabilities().images; + const followUpKey = keyDisplayText("app.message.followUp"); let currentWarnings = { ...config.warnings }; const items: SettingItem[] = [ @@ -225,8 +227,7 @@ export class SettingsSelectorComponent extends Container { { id: "follow-up-mode", label: "Follow-up mode", - description: - "Alt+Enter queues follow-up messages until agent stops. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.", + description: `${followUpKey} queues follow-up messages until agent stops. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.`, currentValue: config.followUpMode, values: ["one-at-a-time", "all"], }, diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index 35c59d74..29962d7e 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -1185,11 +1185,12 @@ export class TreeSelectorComponent extends Container implements Focusable { keyText("app.tree.filter.all"), ].join("/"); const cycleKeys = `${keyText("app.tree.filter.cycleForward")}/${keyText("app.tree.filter.cycleBackward")}`; + const branchKeys = `${keyText("app.tree.foldOrUp")}/${keyText("app.tree.unfoldOrDown")}`; this.addChild( new TruncatedText( theme.fg( "muted", - ` ↑/↓: move. ←/→: page. ^←/^→ or Alt+←/Alt+→: fold/branch. ${keyText("app.tree.editLabel")}: label. ${filterKeys}: filters (${cycleKeys} cycle). ${keyText("app.tree.toggleLabelTimestamp")}: label time`, + ` ↑/↓: move. ←/→: page. ${branchKeys}: fold/branch. ${keyText("app.tree.editLabel")}: label. ${filterKeys}: filters (${cycleKeys} cycle). ${keyText("app.tree.toggleLabelTimestamp")}: label time`, ), 0, 0, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6e2cf1f4..006ee7c5 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -108,7 +108,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js"; import { ExtensionInputComponent } from "./components/extension-input.js"; import { ExtensionSelectorComponent } from "./components/extension-selector.js"; import { FooterComponent } from "./components/footer.js"; -import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js"; +import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js"; import { LoginDialogComponent } from "./components/login-dialog.js"; import { ModelSelectorComponent } from "./components/model-selector.js"; import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.js"; @@ -5137,33 +5137,18 @@ export class InteractiveMode { this.ui.requestRender(); } - /** - * Capitalize keybinding for display (e.g., "ctrl+c" -> "Ctrl+C"). - */ - private capitalizeKey(key: string): string { - return key - .split("/") - .map((k) => - k - .split("+") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join("+"), - ) - .join("/"); - } - /** * Get capitalized display string for an app keybinding action. */ private getAppKeyDisplay(action: AppKeybinding): string { - return this.capitalizeKey(keyText(action)); + return keyDisplayText(action); } /** * Get capitalized display string for an editor keybinding action. */ private getEditorKeyDisplay(action: Keybinding): string { - return this.capitalizeKey(keyText(action)); + return keyDisplayText(action); } private handleHotkeysCommand(): void { @@ -5267,7 +5252,7 @@ export class InteractiveMode { `; for (const [key, shortcut] of shortcuts) { const description = shortcut.description ?? shortcut.extensionPath; - const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase()); + const keyDisplay = formatKeyText(key, { capitalize: true }); hotkeys += `| \`${keyDisplay}\` | ${description} |\n`; } }