Merge pull request #3754 from jeremymcs/fix/os-specific-keyboard-shortcuts

fix(gsd): OS-specific keyboard shortcut hints via formatShortcut helper
This commit is contained in:
Jeremy McSpadden 2026-04-07 21:08:26 -05:00 committed by GitHub
commit b628bfda4f
9 changed files with 102 additions and 9 deletions

View file

@ -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}`)

View file

@ -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)) {

View file

@ -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 <desc> Apply user override to active work",

View file

@ -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. */

View file

@ -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";

View file

@ -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 ──<E29480><E29480><EFBFBD>────────────────────────<E29480><E29480><EFBFBD>─────────────────────────
@ -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 ────────────────────────────────────────────────────────

View file

@ -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.
*/

View file

@ -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 <task>` - 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

View file

@ -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');
});