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:
parent
5c31668760
commit
0fe43de357
2 changed files with 525 additions and 60 deletions
|
|
@ -11,7 +11,11 @@
|
||||||
import { OLLAMA_PROVIDER } from './lib/providers/ollama';
|
import { OLLAMA_PROVIDER } from './lib/providers/ollama';
|
||||||
import { registerMemoryAdapter } from './lib/adapters/memory-adapter';
|
import { registerMemoryAdapter } from './lib/adapters/memory-adapter';
|
||||||
import { MemoraAdapter } from './lib/adapters/memora-bridge';
|
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 { disableWakeScheduler } from './lib/stores/wake-scheduler.svelte';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
|
@ -22,6 +26,7 @@
|
||||||
import SettingsTab from './lib/components/Workspace/SettingsTab.svelte';
|
import SettingsTab from './lib/components/Workspace/SettingsTab.svelte';
|
||||||
import CommsTab from './lib/components/Workspace/CommsTab.svelte';
|
import CommsTab from './lib/components/Workspace/CommsTab.svelte';
|
||||||
import CommandPalette from './lib/components/Workspace/CommandPalette.svelte';
|
import CommandPalette from './lib/components/Workspace/CommandPalette.svelte';
|
||||||
|
import SearchOverlay from './lib/components/Workspace/SearchOverlay.svelte';
|
||||||
|
|
||||||
// Shared
|
// Shared
|
||||||
import StatusBar from './lib/components/StatusBar/StatusBar.svelte';
|
import StatusBar from './lib/components/StatusBar/StatusBar.svelte';
|
||||||
|
|
@ -35,6 +40,7 @@
|
||||||
let detachedConfig = getDetachedConfig();
|
let detachedConfig = getDetachedConfig();
|
||||||
|
|
||||||
let paletteOpen = $state(false);
|
let paletteOpen = $state(false);
|
||||||
|
let searchOpen = $state(false);
|
||||||
let drawerOpen = $state(false);
|
let drawerOpen = $state(false);
|
||||||
let loaded = $state(false);
|
let loaded = $state(false);
|
||||||
|
|
||||||
|
|
@ -93,25 +99,104 @@
|
||||||
loadWorkspace().then(() => { loaded = true; });
|
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) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
// Ctrl+K — command palette
|
// Ctrl+K — command palette (always active)
|
||||||
if (e.ctrlKey && !e.shiftKey && e.key === 'k') {
|
if (e.ctrlKey && !e.shiftKey && e.key === 'k') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
paletteOpen = !paletteOpen;
|
paletteOpen = !paletteOpen;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+1..5 — focus project by index
|
// Ctrl+Shift+F — global search overlay
|
||||||
if (e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '5') {
|
if (e.ctrlKey && e.shiftKey && (e.key === 'F' || e.key === 'f')) {
|
||||||
e.preventDefault();
|
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;
|
const idx = parseInt(e.key) - 1;
|
||||||
if (idx < projects.length) {
|
if (idx < projects.length) {
|
||||||
setActiveProject(projects[idx].id);
|
setActiveProject(projects[idx].id);
|
||||||
|
triggerFocusFlash(projects[idx].id);
|
||||||
}
|
}
|
||||||
return;
|
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
|
// Ctrl+, — toggle settings panel
|
||||||
if (e.ctrlKey && e.key === ',') {
|
if (e.ctrlKey && e.key === ',') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -212,6 +297,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommandPalette open={paletteOpen} onclose={() => paletteOpen = false} />
|
<CommandPalette open={paletteOpen} onclose={() => paletteOpen = false} />
|
||||||
|
<SearchOverlay open={searchOpen} onclose={() => searchOpen = false} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="loading">Loading workspace...</div>
|
<div class="loading">Loading workspace...</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAllGroups, switchGroup, getActiveGroupId } from '../../stores/workspace.svelte';
|
import {
|
||||||
|
getAllGroups,
|
||||||
|
switchGroup,
|
||||||
|
getActiveGroupId,
|
||||||
|
getAllWorkItems,
|
||||||
|
getActiveProjectId,
|
||||||
|
setActiveProject,
|
||||||
|
setActiveTab,
|
||||||
|
triggerFocusFlash,
|
||||||
|
emitProjectTabSwitch,
|
||||||
|
emitTerminalToggle,
|
||||||
|
addTerminalTab,
|
||||||
|
} from '../../stores/workspace.svelte';
|
||||||
|
import { getPluginCommands } from '../../stores/plugins.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -10,32 +23,259 @@
|
||||||
|
|
||||||
let query = $state('');
|
let query = $state('');
|
||||||
let inputEl: HTMLInputElement | undefined = $state();
|
let inputEl: HTMLInputElement | undefined = $state();
|
||||||
|
let selectedIndex = $state(0);
|
||||||
|
let showShortcuts = $state(false);
|
||||||
|
|
||||||
let groups = $derived(getAllGroups());
|
// --- Command definitions ---
|
||||||
let filtered = $derived(
|
|
||||||
groups.filter(g =>
|
interface Command {
|
||||||
g.name.toLowerCase().includes(query.toLowerCase()),
|
id: string;
|
||||||
),
|
label: string;
|
||||||
);
|
category: string;
|
||||||
let activeGroupId = $derived(getActiveGroupId());
|
shortcut?: string;
|
||||||
|
action: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let commands = $derived.by((): Command[] => {
|
||||||
|
const cmds: Command[] = [];
|
||||||
|
const groups = getAllGroups();
|
||||||
|
const projects = getAllWorkItems();
|
||||||
|
const activeGroupId = getActiveGroupId();
|
||||||
|
const activeProjectId = getActiveProjectId();
|
||||||
|
|
||||||
|
// Project focus commands
|
||||||
|
projects.forEach((p, i) => {
|
||||||
|
if (i < 5) {
|
||||||
|
cmds.push({
|
||||||
|
id: `focus-project-${i + 1}`,
|
||||||
|
label: `Focus Project ${i + 1}: ${p.name}`,
|
||||||
|
category: 'Navigation',
|
||||||
|
shortcut: `Alt+${i + 1}`,
|
||||||
|
action: () => {
|
||||||
|
setActiveProject(p.id);
|
||||||
|
triggerFocusFlash(p.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tab switching commands (for active project)
|
||||||
|
const tabNames: [string, number][] = [
|
||||||
|
['Model', 1], ['Docs', 2], ['Context', 3], ['Files', 4],
|
||||||
|
['SSH', 5], ['Memory', 6], ['Metrics', 7],
|
||||||
|
];
|
||||||
|
for (const [name, idx] of tabNames) {
|
||||||
|
cmds.push({
|
||||||
|
id: `tab-${name.toLowerCase()}`,
|
||||||
|
label: `Switch to ${name} Tab`,
|
||||||
|
category: 'Tabs',
|
||||||
|
shortcut: `Ctrl+Shift+${idx}`,
|
||||||
|
action: () => {
|
||||||
|
if (activeProjectId) {
|
||||||
|
emitProjectTabSwitch(activeProjectId, idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal toggle
|
||||||
|
cmds.push({
|
||||||
|
id: 'toggle-terminal',
|
||||||
|
label: 'Toggle Terminal Section',
|
||||||
|
category: 'Tabs',
|
||||||
|
shortcut: 'Ctrl+J',
|
||||||
|
action: () => {
|
||||||
|
if (activeProjectId) {
|
||||||
|
emitTerminalToggle(activeProjectId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// New terminal tab
|
||||||
|
cmds.push({
|
||||||
|
id: 'new-terminal',
|
||||||
|
label: 'New Terminal Tab',
|
||||||
|
category: 'Terminal',
|
||||||
|
action: () => {
|
||||||
|
if (activeProjectId) {
|
||||||
|
addTerminalTab(activeProjectId, {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
title: 'Terminal',
|
||||||
|
type: 'shell',
|
||||||
|
});
|
||||||
|
emitTerminalToggle(activeProjectId); // ensure terminal section is open
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent session commands
|
||||||
|
cmds.push({
|
||||||
|
id: 'focus-agent',
|
||||||
|
label: 'Focus Agent Pane',
|
||||||
|
category: 'Agent',
|
||||||
|
shortcut: 'Ctrl+Shift+K',
|
||||||
|
action: () => {
|
||||||
|
if (activeProjectId) {
|
||||||
|
emitProjectTabSwitch(activeProjectId, 1); // Model tab
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group switching commands
|
||||||
|
for (const group of groups) {
|
||||||
|
cmds.push({
|
||||||
|
id: `group-${group.id}`,
|
||||||
|
label: `Switch Group: ${group.name}`,
|
||||||
|
category: 'Groups',
|
||||||
|
shortcut: group.id === activeGroupId ? '(active)' : undefined,
|
||||||
|
action: () => switchGroup(group.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings toggle
|
||||||
|
cmds.push({
|
||||||
|
id: 'toggle-settings',
|
||||||
|
label: 'Toggle Settings',
|
||||||
|
category: 'UI',
|
||||||
|
shortcut: 'Ctrl+,',
|
||||||
|
action: () => {
|
||||||
|
setActiveTab('settings');
|
||||||
|
// Toggle is handled by App.svelte
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vi navigation
|
||||||
|
cmds.push({
|
||||||
|
id: 'nav-prev-project',
|
||||||
|
label: 'Focus Previous Project',
|
||||||
|
category: 'Navigation',
|
||||||
|
shortcut: 'Ctrl+H',
|
||||||
|
action: () => {
|
||||||
|
const idx = projects.findIndex(p => p.id === activeProjectId);
|
||||||
|
if (idx > 0) {
|
||||||
|
setActiveProject(projects[idx - 1].id);
|
||||||
|
triggerFocusFlash(projects[idx - 1].id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cmds.push({
|
||||||
|
id: 'nav-next-project',
|
||||||
|
label: 'Focus Next Project',
|
||||||
|
category: 'Navigation',
|
||||||
|
shortcut: 'Ctrl+L',
|
||||||
|
action: () => {
|
||||||
|
const idx = projects.findIndex(p => p.id === activeProjectId);
|
||||||
|
if (idx >= 0 && idx < projects.length - 1) {
|
||||||
|
setActiveProject(projects[idx + 1].id);
|
||||||
|
triggerFocusFlash(projects[idx + 1].id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts help
|
||||||
|
cmds.push({
|
||||||
|
id: 'shortcuts-help',
|
||||||
|
label: 'Keyboard Shortcuts',
|
||||||
|
category: 'Help',
|
||||||
|
shortcut: '?',
|
||||||
|
action: () => { showShortcuts = true; },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plugin-registered commands
|
||||||
|
for (const pc of getPluginCommands()) {
|
||||||
|
cmds.push({
|
||||||
|
id: `plugin-${pc.pluginId}-${pc.label.toLowerCase().replace(/\s+/g, '-')}`,
|
||||||
|
label: pc.label,
|
||||||
|
category: 'Plugins',
|
||||||
|
action: pc.callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmds;
|
||||||
|
});
|
||||||
|
|
||||||
|
let filtered = $derived.by((): Command[] => {
|
||||||
|
if (!query.trim()) return commands;
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
return commands.filter(c =>
|
||||||
|
c.label.toLowerCase().includes(q) ||
|
||||||
|
c.category.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Grouped for display
|
||||||
|
let grouped = $derived.by((): [string, Command[]][] => {
|
||||||
|
const map = new Map<string, Command[]>();
|
||||||
|
for (const cmd of filtered) {
|
||||||
|
const list = map.get(cmd.category) ?? [];
|
||||||
|
list.push(cmd);
|
||||||
|
map.set(cmd.category, list);
|
||||||
|
}
|
||||||
|
return [...map.entries()];
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
query = '';
|
query = '';
|
||||||
// Focus input after render
|
selectedIndex = 0;
|
||||||
|
showShortcuts = false;
|
||||||
requestAnimationFrame(() => inputEl?.focus());
|
requestAnimationFrame(() => inputEl?.focus());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function selectGroup(groupId: string) {
|
// Reset selection when filter changes
|
||||||
switchGroup(groupId);
|
$effect(() => {
|
||||||
|
void filtered;
|
||||||
|
selectedIndex = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function executeCommand(cmd: Command) {
|
||||||
|
cmd.action();
|
||||||
onclose();
|
onclose();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onclose();
|
if (showShortcuts) {
|
||||||
|
showShortcuts = false;
|
||||||
|
e.stopPropagation();
|
||||||
|
} else {
|
||||||
|
onclose();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showShortcuts) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filtered.length - 1);
|
||||||
|
scrollToSelected();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
scrollToSelected();
|
||||||
|
} else if (e.key === 'Enter' && filtered.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
executeCommand(filtered[selectedIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToSelected() {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = document.querySelector('.palette-item.selected');
|
||||||
|
el?.scrollIntoView({ block: 'nearest' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track flat index across grouped display
|
||||||
|
function getFlatIndex(groupIdx: number, itemIdx: number): number {
|
||||||
|
let idx = 0;
|
||||||
|
for (let g = 0; g < groupIdx; g++) {
|
||||||
|
idx += grouped[g][1].length;
|
||||||
|
}
|
||||||
|
return idx + itemIdx;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -44,31 +284,77 @@
|
||||||
<div class="palette-backdrop" onclick={onclose} onkeydown={handleKeydown}>
|
<div class="palette-backdrop" onclick={onclose} onkeydown={handleKeydown}>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="palette" data-testid="command-palette" onclick={(e) => e.stopPropagation()} onkeydown={handleKeydown}>
|
<div class="palette" data-testid="command-palette" onclick={(e) => e.stopPropagation()} onkeydown={handleKeydown}>
|
||||||
<input
|
{#if showShortcuts}
|
||||||
bind:this={inputEl}
|
<div class="shortcuts-header">
|
||||||
bind:value={query}
|
<h3>Keyboard Shortcuts</h3>
|
||||||
class="palette-input"
|
<button class="shortcuts-close" onclick={() => showShortcuts = false}>
|
||||||
data-testid="palette-input"
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
placeholder="Switch group..."
|
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
onkeydown={handleKeydown}
|
</svg>
|
||||||
/>
|
</button>
|
||||||
<ul class="palette-results">
|
</div>
|
||||||
{#each filtered as group}
|
<div class="shortcuts-list">
|
||||||
<li>
|
<div class="shortcut-section">
|
||||||
<button
|
<h4>Global</h4>
|
||||||
class="palette-item"
|
<div class="shortcut-row"><kbd>Ctrl+K</kbd><span>Command Palette</span></div>
|
||||||
class:active={group.id === activeGroupId}
|
<div class="shortcut-row"><kbd>Ctrl+,</kbd><span>Toggle Settings</span></div>
|
||||||
onclick={() => selectGroup(group.id)}
|
<div class="shortcut-row"><kbd>Ctrl+M</kbd><span>Toggle Messages</span></div>
|
||||||
>
|
<div class="shortcut-row"><kbd>Ctrl+B</kbd><span>Toggle Sidebar</span></div>
|
||||||
<span class="group-name">{group.name}</span>
|
<div class="shortcut-row"><kbd>Escape</kbd><span>Close Panel / Palette</span></div>
|
||||||
<span class="project-count">{group.projects.length} projects</span>
|
</div>
|
||||||
</button>
|
<div class="shortcut-section">
|
||||||
</li>
|
<h4>Project Navigation</h4>
|
||||||
{/each}
|
<div class="shortcut-row"><kbd>Alt+1</kbd> – <kbd>Alt+5</kbd><span>Focus Project 1–5</span></div>
|
||||||
{#if filtered.length === 0}
|
<div class="shortcut-row"><kbd>Ctrl+H</kbd><span>Previous Project</span></div>
|
||||||
<li class="no-results">No groups match "{query}"</li>
|
<div class="shortcut-row"><kbd>Ctrl+L</kbd><span>Next Project</span></div>
|
||||||
{/if}
|
<div class="shortcut-row"><kbd>Ctrl+J</kbd><span>Toggle Terminal</span></div>
|
||||||
</ul>
|
<div class="shortcut-row"><kbd>Ctrl+Shift+K</kbd><span>Focus Agent Pane</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-section">
|
||||||
|
<h4>Project Tabs</h4>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+1</kbd><span>Model</span></div>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+2</kbd><span>Docs</span></div>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+3</kbd><span>Context</span></div>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+4</kbd><span>Files</span></div>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+5</kbd><span>SSH</span></div>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+6</kbd><span>Memory</span></div>
|
||||||
|
<div class="shortcut-row"><kbd>Ctrl+Shift+7</kbd><span>Metrics</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
bind:this={inputEl}
|
||||||
|
bind:value={query}
|
||||||
|
class="palette-input"
|
||||||
|
data-testid="palette-input"
|
||||||
|
placeholder="Type a command..."
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
/>
|
||||||
|
<ul class="palette-results">
|
||||||
|
{#each grouped as [category, items], gi}
|
||||||
|
<li class="palette-category">{category}</li>
|
||||||
|
{#each items as cmd, ci}
|
||||||
|
{@const flatIdx = getFlatIndex(gi, ci)}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="palette-item"
|
||||||
|
class:selected={flatIdx === selectedIndex}
|
||||||
|
onclick={() => executeCommand(cmd)}
|
||||||
|
onmouseenter={() => selectedIndex = flatIdx}
|
||||||
|
>
|
||||||
|
<span class="cmd-label">{cmd.label}</span>
|
||||||
|
{#if cmd.shortcut}
|
||||||
|
<kbd class="cmd-shortcut">{cmd.shortcut}</kbd>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{#if filtered.length === 0}
|
||||||
|
<li class="no-results">No commands match "{query}"</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -77,20 +363,20 @@
|
||||||
.palette-backdrop {
|
.palette-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: color-mix(in srgb, var(--ctp-crust) 70%, transparent);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 15vh;
|
padding-top: 12vh;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette {
|
.palette {
|
||||||
width: 28.75rem;
|
width: 32rem;
|
||||||
max-height: 22.5rem;
|
max-height: 28rem;
|
||||||
background: var(--ctp-mantle);
|
background: var(--ctp-mantle);
|
||||||
border: 1px solid var(--ctp-surface1);
|
border: 1px solid var(--ctp-surface1);
|
||||||
border-radius: 0.5rem;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -103,8 +389,9 @@
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--ctp-surface0);
|
border-bottom: 1px solid var(--ctp-surface0);
|
||||||
color: var(--ctp-text);
|
color: var(--ctp-text);
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-input::placeholder {
|
.palette-input::placeholder {
|
||||||
|
|
@ -118,37 +405,55 @@
|
||||||
overflow-y: auto;
|
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 {
|
.palette-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.4rem 0.75rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--ctp-text);
|
color: var(--ctp-text);
|
||||||
font-size: 0.85rem;
|
font-size: 0.82rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0.25rem;
|
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);
|
background: var(--ctp-surface0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item.active {
|
.palette-item.selected {
|
||||||
background: var(--ctp-surface0);
|
outline: 1px solid var(--ctp-blue);
|
||||||
border-left: 3px solid var(--ctp-blue);
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-name {
|
.cmd-label {
|
||||||
font-weight: 600;
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-count {
|
.cmd-shortcut {
|
||||||
color: var(--ctp-overlay0);
|
font-size: 0.68rem;
|
||||||
font-size: 0.75rem;
|
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 {
|
.no-results {
|
||||||
|
|
@ -157,4 +462,78 @@
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
text-align: center;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue