diff --git a/v2/src/App.svelte b/v2/src/App.svelte index efd931a..634cc69 100644 --- a/v2/src/App.svelte +++ b/v2/src/App.svelte @@ -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 @@ paletteOpen = false} /> + searchOpen = false} /> {:else}
Loading workspace...
{/if} diff --git a/v2/src/lib/components/Workspace/CommandPalette.svelte b/v2/src/lib/components/Workspace/CommandPalette.svelte index c48d2e3..ede93bf 100644 --- a/v2/src/lib/components/Workspace/CommandPalette.svelte +++ b/v2/src/lib/components/Workspace/CommandPalette.svelte @@ -1,5 +1,18 @@ @@ -44,31 +284,77 @@
e.stopPropagation()} onkeydown={handleKeydown}> - -
    - {#each filtered as group} -
  • - -
  • - {/each} - {#if filtered.length === 0} -
  • No groups match "{query}"
  • - {/if} -
+ {#if showShortcuts} +
+

Keyboard Shortcuts

+ +
+
+
+

Global

+
Ctrl+KCommand Palette
+
Ctrl+,Toggle Settings
+
Ctrl+MToggle Messages
+
Ctrl+BToggle Sidebar
+
EscapeClose Panel / Palette
+
+
+

Project Navigation

+
Alt+1Alt+5Focus Project 1–5
+
Ctrl+HPrevious Project
+
Ctrl+LNext Project
+
Ctrl+JToggle Terminal
+
Ctrl+Shift+KFocus Agent Pane
+
+
+

Project Tabs

+
Ctrl+Shift+1Model
+
Ctrl+Shift+2Docs
+
Ctrl+Shift+3Context
+
Ctrl+Shift+4Files
+
Ctrl+Shift+5SSH
+
Ctrl+Shift+6Memory
+
Ctrl+Shift+7Metrics
+
+
+ {:else} + +
    + {#each grouped as [category, items], gi} +
  • {category}
  • + {#each items as cmd, ci} + {@const flatIdx = getFlatIndex(gi, ci)} +
  • + +
  • + {/each} + {/each} + {#if filtered.length === 0} +
  • No commands match "{query}"
  • + {/if} +
+ {/if}
{/if} @@ -77,20 +363,20 @@ .palette-backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: color-mix(in srgb, var(--ctp-crust) 70%, transparent); display: flex; justify-content: center; - padding-top: 15vh; + padding-top: 12vh; z-index: 1000; } .palette { - width: 28.75rem; - max-height: 22.5rem; + width: 32rem; + max-height: 28rem; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.5rem; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; overflow: hidden; @@ -103,8 +389,9 @@ border: none; border-bottom: 1px solid var(--ctp-surface0); color: var(--ctp-text); - font-size: 0.95rem; + font-size: 0.9rem; outline: none; + font-family: inherit; } .palette-input::placeholder { @@ -118,37 +405,55 @@ overflow-y: auto; } + .palette-category { + padding: 0.375rem 0.75rem 0.125rem; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ctp-overlay0); + } + .palette-item { display: flex; align-items: center; justify-content: space-between; width: 100%; - padding: 0.5rem 0.75rem; + padding: 0.4rem 0.75rem; background: transparent; border: none; color: var(--ctp-text); - font-size: 0.85rem; + font-size: 0.82rem; cursor: pointer; border-radius: 0.25rem; - transition: background 0.1s; + transition: background 0.08s; + font-family: inherit; } - .palette-item:hover { + .palette-item:hover, + .palette-item.selected { background: var(--ctp-surface0); } - .palette-item.active { - background: var(--ctp-surface0); - border-left: 3px solid var(--ctp-blue); + .palette-item.selected { + outline: 1px solid var(--ctp-blue); + outline-offset: -1px; } - .group-name { - font-weight: 600; + .cmd-label { + flex: 1; + text-align: left; } - .project-count { - color: var(--ctp-overlay0); - font-size: 0.75rem; + .cmd-shortcut { + font-size: 0.68rem; + color: var(--ctp-overlay1); + background: var(--ctp-surface1); + padding: 0.1rem 0.375rem; + border-radius: 0.1875rem; + font-family: var(--font-mono, monospace); + white-space: nowrap; + margin-left: 0.5rem; } .no-results { @@ -157,4 +462,78 @@ font-size: 0.85rem; text-align: center; } + + /* Shortcuts overlay */ + .shortcuts-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 1rem; + border-bottom: 1px solid var(--ctp-surface0); + } + + .shortcuts-header h3 { + margin: 0; + font-size: 0.85rem; + font-weight: 600; + color: var(--ctp-text); + } + + .shortcuts-close { + display: flex; + align-items: center; + justify-content: center; + width: 1.375rem; + height: 1.375rem; + background: transparent; + border: none; + border-radius: 0.25rem; + color: var(--ctp-subtext0); + cursor: pointer; + } + + .shortcuts-close:hover { + color: var(--ctp-text); + background: var(--ctp-surface0); + } + + .shortcuts-list { + padding: 0.5rem 1rem; + overflow-y: auto; + } + + .shortcut-section { + margin-bottom: 0.75rem; + } + + .shortcut-section h4 { + margin: 0 0 0.25rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ctp-overlay0); + } + + .shortcut-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0; + font-size: 0.8rem; + color: var(--ctp-subtext1); + } + + .shortcut-row kbd { + font-size: 0.68rem; + color: var(--ctp-overlay1); + background: var(--ctp-surface1); + padding: 0.1rem 0.375rem; + border-radius: 0.1875rem; + font-family: var(--font-mono, monospace); + } + + .shortcut-row span { + text-align: right; + }