mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
fix(review): wire Session Manager to /api/sessions/unified contract, stop Ctrl+K PTY leak, finish shortcut registry (PR #146)
- Session Manager (COD-121/192): align _loadSessionManagerList() with the merged #139 endpoint — map UnifiedSessionItem fields (lastActivityAt epoch-ms → lastModified, optional sizeBytes/firstPrompt/name) to the history-record shape _buildHistoryItem renders; surface non-2xx / error-envelope responses as a visible message instead of a silent "No sessions found"; route clicks by liveness (live row → selectSession, history row → resumeHistorySession by conversation UUID) via a new onActivate option so a live session is never duplicate-resumed - Ctrl+K double-dispatch: gate the palette chord in attachCustomKeyEventHandler (return false on keydown) so xterm never writes 0x0b kill-line into the PTY while the palette opens; gate is registry-aware so a rebound/disabled palette shortcut restores normal terminal Ctrl+K - Shortcut registry (COD-157) finished per maintainer decision: document keydown now dispatches through getShortcutRegistry() + matchesShortcutEvent() (legacy SHORTCUTS table removed), honoring per-shortcut disable and rebinds incl. the palette chord; overrides persist via saveAppSettingsToStorage() (correct device key + cache coherence, was orphaned 'codeman:settings'); Shortcuts tab renders on open via switchSettingsTab hook; capture uses a persistent listener that ignores bare modifier keydowns (combos now capturable) and requires a Ctrl/Cmd/Alt chord; settings rows use delegated listeners instead of inline onclick (JS-string injection sink) and overrides can no longer clobber id/label/action; added the missing row + overlay CSS - matchesShortcutEvent: reject undeclared extra modifiers (Ctrl+Shift+K no longer hijacked from Firefox devtools) while keeping Ctrl/Cmd interchangeable; match physical code OR produced key for layout parity - Registry/dispatch gaps: added restore-terminal-size entry, documented Ctrl+Shift+R again in the help modal (test flipped to assert presence), Ctrl+?/Alt+? now really open the registry-driven shortcut overlay, and Escape closes it - Palette new-session pick routes through selectQuickStartCase() so the searchable combobox, dir display, and lastUsedCase stay in sync - Removed fork cherry-pick debris: dead _onSessionListMaybeChanged(), orphaned .session-row-menu CSS, nonexistent closeMobileHeaderUtilities calls - Tests: functional vm-harness coverage for the unified-list field mapping + error state + liveness routing, palette chord shift/disable/ rebind handling, override persistence round-trip, capture flow, tab render hook, and source guards for the PTY gate + registry dispatch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+60
-31
@@ -288,6 +288,7 @@ const DEFAULT_SHORTCUTS = [
|
||||
label: 'Show Shortcuts',
|
||||
bindings: [
|
||||
{ modifiers: ['ctrl'], key: '?', code: 'Slash' },
|
||||
{ modifiers: ['ctrl', 'shift'], key: '?' },
|
||||
{ modifiers: ['alt'], key: '?', code: 'Slash' },
|
||||
],
|
||||
action: 'showShortcutOverlay',
|
||||
@@ -337,6 +338,13 @@ const DEFAULT_SHORTCUTS = [
|
||||
bindings: [{ modifiers: ['ctrl', 'shift'], key: 'V' }],
|
||||
action: 'toggleVoiceInput',
|
||||
},
|
||||
{
|
||||
id: 'restore-terminal-size',
|
||||
group: 'Terminal',
|
||||
label: 'Restore Terminal Size',
|
||||
bindings: [{ modifiers: ['ctrl', 'shift'], key: 'R' }],
|
||||
action: 'restoreTerminalSize',
|
||||
},
|
||||
{
|
||||
id: 'move-tab-left',
|
||||
group: 'Tabs',
|
||||
@@ -903,21 +911,23 @@ class CodemanApp {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
setupEventListeners() {
|
||||
// Keyboard shortcut lookup table — data-driven to avoid 12 separate if-blocks.
|
||||
// Each entry: { key, altKey? (alternative key match), ctrl? (require Ctrl/Cmd),
|
||||
// shift? (require Shift), action }.
|
||||
const SHORTCUTS = [
|
||||
{ key: '?', altKey: '/', ctrl: true, action: () => this.showHelp() },
|
||||
{ key: 'w', ctrl: true, action: () => this.killActiveSession() },
|
||||
{ key: 'Tab', ctrl: true, action: () => this.nextSession() },
|
||||
{ key: 'l', ctrl: true, action: () => this.clearTerminal() },
|
||||
{ key: 'R', ctrl: true, shift: true, action: () => this.restoreTerminalSize() },
|
||||
{ key: '=', altKey: '+', ctrl: true, action: () => this.increaseFontSize() },
|
||||
{ key: '-', ctrl: true, action: () => this.decreaseFontSize() },
|
||||
{ key: 'V', ctrl: true, shift: true, action: () => VoiceInput.toggle() },
|
||||
{ key: '{', ctrl: true, shift: true, action: () => this.moveActiveTabLeft() },
|
||||
{ key: '}', ctrl: true, shift: true, action: () => this.moveActiveTabRight() },
|
||||
];
|
||||
// Action name → handler map for the shortcut registry (DEFAULT_SHORTCUTS +
|
||||
// user overrides from settings.shortcutOverrides, merged by
|
||||
// getShortcutRegistry()). The command palette chord is deliberately NOT in
|
||||
// this map — shouldOpenCommandPaletteFromShortcut() dispatches it above with
|
||||
// focus-target awareness (it must fire from the terminal but not from inputs).
|
||||
const SHORTCUT_ACTIONS = {
|
||||
showShortcutOverlay: () => this.showShortcutOverlay(),
|
||||
killActiveSession: () => this.killActiveSession(),
|
||||
nextSession: () => this.nextSession(),
|
||||
clearTerminal: () => this.clearTerminal(),
|
||||
restoreTerminalSize: () => this.restoreTerminalSize(),
|
||||
increaseFontSize: () => this.increaseFontSize(),
|
||||
decreaseFontSize: () => this.decreaseFontSize(),
|
||||
toggleVoiceInput: () => VoiceInput.toggle(),
|
||||
moveActiveTabLeft: () => this.moveActiveTabLeft(),
|
||||
moveActiveTabRight: () => this.moveActiveTabRight(),
|
||||
};
|
||||
|
||||
// Use capture to handle before terminal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -937,6 +947,7 @@ class CodemanApp {
|
||||
if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory();
|
||||
this.closeSessionManager();
|
||||
this.closeCommandPalette?.();
|
||||
this.closeShortcutOverlay?.();
|
||||
}
|
||||
|
||||
// Option/Alt session navigation uses physical key CODES, not e.key, so macOS
|
||||
@@ -966,14 +977,18 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
// Match against shortcut table
|
||||
for (const s of SHORTCUTS) {
|
||||
const keyMatch = e.key === s.key || (s.altKey && e.key === s.altKey);
|
||||
const ctrlMatch = s.ctrl ? (e.ctrlKey || e.metaKey) : true;
|
||||
const shiftMatch = s.shift ? e.shiftKey : !e.shiftKey;
|
||||
if (keyMatch && ctrlMatch && shiftMatch) {
|
||||
// Match against the shortcut registry so user rebinds and per-shortcut
|
||||
// disables (App Settings → Shortcuts) take effect. Every dispatchable
|
||||
// binding requires Ctrl/Cmd/Alt (capture enforces the same), so plain
|
||||
// typing exits early without touching the registry.
|
||||
if (!e.ctrlKey && !e.metaKey && !e.altKey) return;
|
||||
for (const shortcut of this.getShortcutRegistry()) {
|
||||
if (shortcut.disabled || !shortcut.action) continue;
|
||||
const action = SHORTCUT_ACTIONS[shortcut.action];
|
||||
if (!action) continue;
|
||||
if (this.matchesShortcutEvent(e, shortcut)) {
|
||||
e.preventDefault();
|
||||
s.action();
|
||||
action();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4360,21 +4375,35 @@ class CodemanApp {
|
||||
return DEFAULT_SHORTCUTS.map((shortcut) => {
|
||||
const override = shortcutOverrides[shortcut.id];
|
||||
if (!override) return shortcut;
|
||||
return { ...shortcut, ...override };
|
||||
// Only binding-shaped fields may come from storage — id/label/group/action
|
||||
// stay trusted so persisted data can never redirect a shortcut's action or
|
||||
// spoof another row in the settings/overlay renderers.
|
||||
const merged = { ...shortcut };
|
||||
if (Array.isArray(override.bindings)) {
|
||||
merged.bindings = override.bindings;
|
||||
delete merged.displayBindings; // show the override, not the stale default label
|
||||
}
|
||||
if (typeof override.disabled === 'boolean') merged.disabled = override.disabled;
|
||||
return merged;
|
||||
});
|
||||
}
|
||||
|
||||
matchesShortcutEvent(e, shortcut) {
|
||||
if (!shortcut.bindings) return false;
|
||||
if (!shortcut || !Array.isArray(shortcut.bindings)) return false;
|
||||
return shortcut.bindings.some((binding) => {
|
||||
const mods = binding.modifiers || [];
|
||||
if (mods.includes('ctrl') && !e.ctrlKey) return false;
|
||||
if (mods.includes('meta') && !e.metaKey) return false;
|
||||
if (mods.includes('shift') && !e.shiftKey) return false;
|
||||
if (mods.includes('alt') && !e.altKey) return false;
|
||||
if (!mods.includes('ctrl') && !mods.includes('meta') && (e.ctrlKey || e.metaKey)) return false;
|
||||
if (binding.code) return e.code === binding.code;
|
||||
if (binding.key) return e.key === binding.key || e.key.toLowerCase() === binding.key.toLowerCase();
|
||||
// Ctrl and Cmd are interchangeable as the primary modifier (parity with
|
||||
// the legacy shortcut table), but every OTHER pressed modifier must be
|
||||
// declared by the binding — a plain Ctrl+K binding must not also swallow
|
||||
// Ctrl+Shift+K (the Firefox devtools chord).
|
||||
const wantsPrimary = mods.includes('ctrl') || mods.includes('meta');
|
||||
if (wantsPrimary !== !!(e.ctrlKey || e.metaKey)) return false;
|
||||
if (mods.includes('shift') !== !!e.shiftKey) return false;
|
||||
if (mods.includes('alt') !== !!e.altKey) return false;
|
||||
// Match the physical key when the binding pins one (layout-independent),
|
||||
// or the produced character otherwise (layout-dependent keys like '+').
|
||||
if (binding.code && e.code === binding.code) return true;
|
||||
if (binding.key && typeof e.key === 'string' && e.key.toLowerCase() === binding.key.toLowerCase()) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user