feat: add keyboard-first UX and rewrite CommandPalette

Alt+1-5 project jump, Ctrl+H/L vi-nav, Ctrl+Shift+1-9 tab switch,
Ctrl+J terminal toggle, Ctrl+Shift+K focus agent. isEditing() guard.
CommandPalette: 18+ commands, 6 categories, fuzzy filter, arrow nav.
This commit is contained in:
Hibryda 2026-03-12 04:57:29 +01:00 committed by DexterFromLab
parent d31a2c3ed7
commit a9b7ed0dda
2 changed files with 525 additions and 60 deletions

View file

@ -11,7 +11,11 @@
import { OLLAMA_PROVIDER } from './lib/providers/ollama';
import { registerMemoryAdapter } from './lib/adapters/memory-adapter';
import { MemoraAdapter } from './lib/adapters/memora-bridge';
import { loadWorkspace, getActiveTab, setActiveTab, setActiveProject, getEnabledProjects } from './lib/stores/workspace.svelte';
import {
loadWorkspace, getActiveTab, setActiveTab, setActiveProject,
getEnabledProjects, getAllWorkItems, getActiveProjectId,
triggerFocusFlash, emitProjectTabSwitch, emitTerminalToggle,
} from './lib/stores/workspace.svelte';
import { disableWakeScheduler } from './lib/stores/wake-scheduler.svelte';
import { invoke } from '@tauri-apps/api/core';
@ -22,6 +26,7 @@
import SettingsTab from './lib/components/Workspace/SettingsTab.svelte';
import CommsTab from './lib/components/Workspace/CommsTab.svelte';
import CommandPalette from './lib/components/Workspace/CommandPalette.svelte';
import SearchOverlay from './lib/components/Workspace/SearchOverlay.svelte';
// Shared
import StatusBar from './lib/components/StatusBar/StatusBar.svelte';
@ -35,6 +40,7 @@
let detachedConfig = getDetachedConfig();
let paletteOpen = $state(false);
let searchOpen = $state(false);
let drawerOpen = $state(false);
let loaded = $state(false);
@ -93,25 +99,104 @@
loadWorkspace().then(() => { loaded = true; });
}
/** Check if event target is an editable element (input, textarea, contenteditable) */
function isEditing(e: KeyboardEvent): boolean {
const t = e.target as HTMLElement;
if (!t) return false;
const tag = t.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return true;
if (t.isContentEditable) return true;
// xterm.js canvases and textareas should be considered editing
if (t.closest('.xterm')) return true;
return false;
}
function handleKeydown(e: KeyboardEvent) {
// Ctrl+K — command palette
// Ctrl+K — command palette (always active)
if (e.ctrlKey && !e.shiftKey && e.key === 'k') {
e.preventDefault();
paletteOpen = !paletteOpen;
return;
}
// Ctrl+1..5 — focus project by index
if (e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '5') {
// Ctrl+Shift+F — global search overlay
if (e.ctrlKey && e.shiftKey && (e.key === 'F' || e.key === 'f')) {
e.preventDefault();
const projects = getEnabledProjects();
searchOpen = !searchOpen;
return;
}
// Alt+1..5 — quick-jump to project by index
if (e.altKey && !e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '5') {
e.preventDefault();
const projects = getAllWorkItems();
const idx = parseInt(e.key) - 1;
if (idx < projects.length) {
setActiveProject(projects[idx].id);
triggerFocusFlash(projects[idx].id);
}
return;
}
// Ctrl+Shift+1..9 — switch tab within focused project
if (e.ctrlKey && e.shiftKey && e.key >= '1' && e.key <= '9') {
// Allow Ctrl+Shift+K to pass through to its own handler
if (e.key === 'K') return;
e.preventDefault();
const projectId = getActiveProjectId();
if (projectId) {
const tabIdx = parseInt(e.key);
emitProjectTabSwitch(projectId, tabIdx);
}
return;
}
// Ctrl+Shift+K — focus agent pane (switch to Model tab)
if (e.ctrlKey && e.shiftKey && (e.key === 'K' || e.key === 'k')) {
e.preventDefault();
const projectId = getActiveProjectId();
if (projectId) {
emitProjectTabSwitch(projectId, 1); // Model tab
}
return;
}
// Vi-style navigation (skip when editing text)
if (e.ctrlKey && !e.shiftKey && !e.altKey && !isEditing(e)) {
const projects = getAllWorkItems();
const currentId = getActiveProjectId();
const currentIdx = projects.findIndex(p => p.id === currentId);
// Ctrl+H — focus previous project (left)
if (e.key === 'h') {
e.preventDefault();
if (currentIdx > 0) {
setActiveProject(projects[currentIdx - 1].id);
triggerFocusFlash(projects[currentIdx - 1].id);
}
return;
}
// Ctrl+L — focus next project (right)
if (e.key === 'l') {
e.preventDefault();
if (currentIdx >= 0 && currentIdx < projects.length - 1) {
setActiveProject(projects[currentIdx + 1].id);
triggerFocusFlash(projects[currentIdx + 1].id);
}
return;
}
// Ctrl+J — toggle terminal section in focused project
if (e.key === 'j') {
e.preventDefault();
if (currentId) {
emitTerminalToggle(currentId);
}
return;
}
}
// Ctrl+, — toggle settings panel
if (e.ctrlKey && e.key === ',') {
e.preventDefault();
@ -212,6 +297,7 @@
</div>
<CommandPalette open={paletteOpen} onclose={() => paletteOpen = false} />
<SearchOverlay open={searchOpen} onclose={() => searchOpen = false} />
{:else}
<div class="loading">Loading workspace...</div>
{/if}