fix(gsd): OS-specific keyboard shortcut hints via formatShortcut helper
Keyboard shortcut hints were hardcoded as Ctrl+Alt+X everywhere except auto-dashboard.ts which had an inline platform check. On macOS these should render as ⌃⌥X. - Add formatShortcut() to files.ts — converts Ctrl/Alt/Shift/Cmd modifiers to macOS symbols (⌃/⌥/⇧/⌘) when process.platform is darwin - Replace all inline platform checks and hardcoded hints with formatShortcut() calls - Use template variables in system.md for shortcut hints - Update comments in overlay files for consistency - Add 7 tests covering all modifier conversions and passthrough Closes #3753
This commit is contained in:
parent
a3ea23bda1
commit
e52be4fc09
9 changed files with 102 additions and 9 deletions
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
69
src/resources/extensions/gsd/tests/format-shortcut.test.ts
Normal file
69
src/resources/extensions/gsd/tests/format-shortcut.test.ts
Normal 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');
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue