diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index e59e34146..00baa93fa 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -16,6 +16,7 @@ import { resolveSliceFile, } from "./paths.js"; import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js"; +import { formatShortcut } from "./files.js"; import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; @@ -855,7 +856,7 @@ export function updateProgressWidget( // Hints line const hintParts: string[] = []; hintParts.push("esc pause"); - hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard"); + hintParts.push(`${formatShortcut("Ctrl+Alt+G")} dashboard`); const hintStr = theme.fg("dim", hintParts.join(" | ")); const commitStr = lastCommit ? theme.fg("dim", `${lastCommit.timeAgo} ago: ${commitMsg}`) diff --git a/src/resources/extensions/gsd/bootstrap/system-context.ts b/src/resources/extensions/gsd/bootstrap/system-context.ts index c82217449..7d16b3b00 100644 --- a/src/resources/extensions/gsd/bootstrap/system-context.ts +++ b/src/resources/extensions/gsd/bootstrap/system-context.ts @@ -15,7 +15,7 @@ import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "../skill-dis import { getActiveAutoWorktreeContext } from "../auto-worktree.js"; import { getActiveWorktreeName, getWorktreeOriginalCwd } from "../worktree-command.js"; import { deriveState } from "../state.js"; -import { formatOverridesSection, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js"; +import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js"; import { toPosixPath } from "../../shared/mod.js"; import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../../cmux/index.js"; @@ -72,6 +72,8 @@ export async function buildBeforeAgentStartResult( const systemContent = loadPrompt("system", { bundledSkillsTable: buildBundledSkillsTable(), templatesDir: getTemplatesDir(), + shortcutDashboard: formatShortcut("Ctrl+Alt+G"), + shortcutShell: formatShortcut("Ctrl+Alt+B"), }); const loadedPreferences = loadEffectiveGSDPreferences(); if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) { diff --git a/src/resources/extensions/gsd/commands/handlers/core.ts b/src/resources/extensions/gsd/commands/handlers/core.ts index 6ddc51107..e321dca4f 100644 --- a/src/resources/extensions/gsd/commands/handlers/core.ts +++ b/src/resources/extensions/gsd/commands/handlers/core.ts @@ -8,6 +8,7 @@ import { runEnvironmentChecks } from "../../doctor-environment.js"; import { deriveState } from "../../state.js"; import { handleCmux } from "../../commands-cmux.js"; import { projectRoot } from "../context.js"; +import { formatShortcut } from "../../files.js"; export function showHelp(ctx: ExtensionCommandContext): void { const lines = [ @@ -24,12 +25,12 @@ export function showHelp(ctx: ExtensionCommandContext): void { " /gsd new-milestone Create milestone from headless context (used by gsd headless)", "", "VISIBILITY", - " /gsd status Show progress dashboard (Ctrl+Alt+G)", + ` /gsd status Show progress dashboard (${formatShortcut("Ctrl+Alt+G")})`, " /gsd visualize Interactive 10-tab TUI (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)", " /gsd queue Show queued/dispatched units and execution order", " /gsd history View execution history [--cost] [--phase] [--model] [N]", " /gsd changelog Show categorized release notes [version]", - " /gsd notifications View persistent notification history [clear|tail|filter] (Ctrl+Alt+N)", + ` /gsd notifications View persistent notification history [clear|tail|filter] (${formatShortcut("Ctrl+Alt+N")})`, "", "COURSE CORRECTION", " /gsd steer Apply user override to active work", diff --git a/src/resources/extensions/gsd/files.ts b/src/resources/extensions/gsd/files.ts index 9bd194604..e92876bec 100644 --- a/src/resources/extensions/gsd/files.ts +++ b/src/resources/extensions/gsd/files.ts @@ -70,6 +70,25 @@ export function clearParseCache(): void { for (const cb of _cacheClearCallbacks) cb(); } +// ─── Platform shortcuts ─────────────────────────────────────────────────── + +const IS_MAC = process.platform === "darwin"; + +/** + * Format a keyboard shortcut for the current OS. + * Input: modifier key combo like "Ctrl+Alt+G" + * Output: "⌃⌥G" on macOS, "Ctrl+Alt+G" on Windows/Linux. + */ +export function formatShortcut(combo: string): string { + if (!IS_MAC) return combo; + return combo + .replace(/Ctrl\+Alt\+/i, "⌃⌥") + .replace(/Ctrl\+/i, "⌃") + .replace(/Alt\+/i, "⌥") + .replace(/Shift\+/i, "⇧") + .replace(/Cmd\+/i, "⌘"); +} + // ─── Helpers ─────────────────────────────────────────────────────────────── /** Extract the text after a heading at a given level, up to the next heading of same or higher level. */ diff --git a/src/resources/extensions/gsd/notification-overlay.ts b/src/resources/extensions/gsd/notification-overlay.ts index 886862ec6..1b5e3bec5 100644 --- a/src/resources/extensions/gsd/notification-overlay.ts +++ b/src/resources/extensions/gsd/notification-overlay.ts @@ -1,6 +1,6 @@ // GSD Extension — Notification History Overlay // Scrollable panel showing all persisted notifications with severity filtering. -// Toggled with Ctrl+Alt+N or opened from /gsd notifications. +// Toggled with Ctrl+Alt+N (⌃⌥N on macOS) or opened from /gsd notifications. import type { Theme } from "@gsd/pi-coding-agent"; import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui"; diff --git a/src/resources/extensions/gsd/notification-widget.ts b/src/resources/extensions/gsd/notification-widget.ts index 15ac855e2..8a963be5e 100644 --- a/src/resources/extensions/gsd/notification-widget.ts +++ b/src/resources/extensions/gsd/notification-widget.ts @@ -6,6 +6,7 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent"; import { getUnreadCount, readNotifications } from "./notification-store.js"; +import { formatShortcut } from "./files.js"; // ─── Pure rendering ──���────────────────────────���───────────────────────── @@ -24,7 +25,7 @@ export function buildNotificationWidgetLines(): string[] { ? latest.message.slice(0, msgMax - 1) + "…" : latest.message; - return [` ${icon} [${badge}] ${truncated} (Ctrl+Alt+N to view)`]; + return [` ${icon} [${badge}] ${truncated} (${formatShortcut("Ctrl+Alt+N")} to view)`]; } // ─── Widget init ──────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/parallel-monitor-overlay.ts b/src/resources/extensions/gsd/parallel-monitor-overlay.ts index 4b671f973..1293ebbc7 100644 --- a/src/resources/extensions/gsd/parallel-monitor-overlay.ts +++ b/src/resources/extensions/gsd/parallel-monitor-overlay.ts @@ -2,7 +2,7 @@ * GSD Parallel Monitor Overlay * * Full-screen TUI overlay showing real-time parallel worker progress. - * Opened via `/gsd parallel watch` or Ctrl+Alt+P. + * Opened via `/gsd parallel watch` or Ctrl+Alt+P (⌃⌥P on macOS). * Reads the same data sources as `scripts/parallel-monitor.mjs` but * renders as a native pi-tui overlay with theme integration. */ diff --git a/src/resources/extensions/gsd/prompts/system.md b/src/resources/extensions/gsd/prompts/system.md index a78c82daf..e7847a315 100644 --- a/src/resources/extensions/gsd/prompts/system.md +++ b/src/resources/extensions/gsd/prompts/system.md @@ -131,8 +131,8 @@ Templates showing the expected format for each artifact type are in: - `/gsd status` - progress dashboard overlay - `/gsd queue` - queue future milestones (safe while auto-mode is running) - `/gsd quick ` - quick task with GSD guarantees (atomic commits, state tracking) but no milestone ceremony -- `Ctrl+Alt+G` - toggle dashboard overlay -- `Ctrl+Alt+B` - show shell processes +- `{{shortcutDashboard}}` - toggle dashboard overlay +- `{{shortcutShell}}` - show shell processes ## Execution Heuristics diff --git a/src/resources/extensions/gsd/tests/format-shortcut.test.ts b/src/resources/extensions/gsd/tests/format-shortcut.test.ts new file mode 100644 index 000000000..b6c90e4b1 --- /dev/null +++ b/src/resources/extensions/gsd/tests/format-shortcut.test.ts @@ -0,0 +1,69 @@ +// GSD Extension — formatShortcut tests +// Verifies OS-specific keyboard shortcut rendering. + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { formatShortcut } from '../files.ts'; + +// ─── formatShortcut renders per-platform shortcuts ────────────────────── + +test('formatShortcut: converts Ctrl+Alt combo on macOS', () => { + // formatShortcut uses process.platform at module load time. + // We can only test the current platform's behavior. + const result = formatShortcut('Ctrl+Alt+G'); + if (process.platform === 'darwin') { + assert.strictEqual(result, '⌃⌥G', 'macOS should use ⌃⌥ symbols'); + } else { + assert.strictEqual(result, 'Ctrl+Alt+G', 'non-macOS should pass through unchanged'); + } +}); + +test('formatShortcut: converts Ctrl+Alt+N', () => { + const result = formatShortcut('Ctrl+Alt+N'); + if (process.platform === 'darwin') { + assert.strictEqual(result, '⌃⌥N'); + } else { + assert.strictEqual(result, 'Ctrl+Alt+N'); + } +}); + +test('formatShortcut: converts Ctrl+Alt+B', () => { + const result = formatShortcut('Ctrl+Alt+B'); + if (process.platform === 'darwin') { + assert.strictEqual(result, '⌃⌥B'); + } else { + assert.strictEqual(result, 'Ctrl+Alt+B'); + } +}); + +test('formatShortcut: converts standalone Ctrl modifier', () => { + const result = formatShortcut('Ctrl+C'); + if (process.platform === 'darwin') { + assert.strictEqual(result, '⌃C'); + } else { + assert.strictEqual(result, 'Ctrl+C'); + } +}); + +test('formatShortcut: converts Shift modifier', () => { + const result = formatShortcut('Shift+Tab'); + if (process.platform === 'darwin') { + assert.strictEqual(result, '⇧Tab'); + } else { + assert.strictEqual(result, 'Shift+Tab'); + } +}); + +test('formatShortcut: converts Cmd modifier', () => { + const result = formatShortcut('Cmd+S'); + if (process.platform === 'darwin') { + assert.strictEqual(result, '⌘S'); + } else { + assert.strictEqual(result, 'Cmd+S'); + } +}); + +test('formatShortcut: passes through plain key names', () => { + assert.strictEqual(formatShortcut('Escape'), 'Escape'); + assert.strictEqual(formatShortcut('Enter'), 'Enter'); +});