generated from nhcarrigan/template
feat: add keyboard shortcuts
This commit is contained in:
@@ -43,14 +43,7 @@
|
|||||||
"🔒 Grant tool permissions as needed for security",
|
"🔒 Grant tool permissions as needed for security",
|
||||||
"📌 Pin important conversations for quick access",
|
"📌 Pin important conversations for quick access",
|
||||||
"🎨 Customize your theme and preferences in Settings",
|
"🎨 Customize your theme and preferences in Settings",
|
||||||
],
|
"⌨️ Check the keyboard icon for available shortcuts",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Keyboard Shortcuts",
|
|
||||||
items: [
|
|
||||||
"Ctrl/Cmd + Enter: Send message",
|
|
||||||
"Ctrl/Cmd + K: Clear chat (when supported)",
|
|
||||||
"Escape: Close modals and panels",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onClose }: Props = $props();
|
||||||
|
|
||||||
|
const shortcuts = [
|
||||||
|
{
|
||||||
|
category: "General",
|
||||||
|
items: [
|
||||||
|
{ keys: ["Escape"], description: "Close modals and panels" },
|
||||||
|
{ keys: ["Ctrl", "L"], description: "Clear the terminal" },
|
||||||
|
{ keys: ["Ctrl", ","], description: "Open settings" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Chat",
|
||||||
|
items: [
|
||||||
|
{ keys: ["Enter"], description: "Send message" },
|
||||||
|
{ keys: ["Shift", "Enter"], description: "New line in message" },
|
||||||
|
{ keys: ["Ctrl", "C"], description: "Interrupt/stop response" },
|
||||||
|
{ keys: ["↑"], description: "Previous input from history" },
|
||||||
|
{ keys: ["↓"], description: "Next input from history" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Slash Commands",
|
||||||
|
items: [
|
||||||
|
{ keys: ["↑", "↓"], description: "Navigate command menu" },
|
||||||
|
{ keys: ["Tab"], description: "Complete selected command" },
|
||||||
|
{ keys: ["Escape"], description: "Close command menu" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Permission Prompts",
|
||||||
|
items: [
|
||||||
|
{ keys: ["Enter"], description: "Allow & reconnect" },
|
||||||
|
{ keys: ["Escape"], description: "Dismiss" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
onclick={onClose}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) => e.key === "Escape" && onClose()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] overflow-hidden flex flex-col"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="shortcuts-title"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between p-6 pb-4 border-b border-[var(--border-color)]">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-8 h-8 rounded-lg bg-[var(--accent-primary)]/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-[var(--accent-primary)]"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 3C10.22 3 8.47 3.23 6.86 3.68A2 2 0 005 5.57V18.43a2 2 0 001.86 1.89C8.47 20.77 10.22 21 12 21s3.53-.23 5.14-.68A2 2 0 0019 18.43V5.57a2 2 0 00-1.86-1.89C15.53 3.23 13.78 3 12 3z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 7h.01M12 7h.01M16 7h.01M8 11h.01M12 11h.01M16 11h.01M8 15h8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 id="shortcuts-title" class="text-xl font-semibold text-gray-100">Keyboard Shortcuts</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={onClose}
|
||||||
|
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-y-auto flex-1 p-6 space-y-6">
|
||||||
|
{#each shortcuts as section (section.category)}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
{section.category}
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each section.items as item (item.description)}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between py-2 px-3 bg-[var(--bg-secondary)] rounded-lg"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">{item.description}</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{#each item.keys as key, i (key)}
|
||||||
|
{#if i > 0}
|
||||||
|
<span class="text-gray-600 text-xs">+</span>
|
||||||
|
{/if}
|
||||||
|
<kbd
|
||||||
|
class="px-2 py-1 text-xs font-mono bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-gray-300 shadow-sm min-w-[24px] text-center"
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</kbd>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[role="dialog"] {
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-color) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -109,8 +109,22 @@ Please continue where we left off and retry that action now that you have permis
|
|||||||
function isToolAlreadyGranted(toolName: string): boolean {
|
function isToolAlreadyGranted(toolName: string): boolean {
|
||||||
return grantedToolsList.includes(toolName);
|
return grantedToolsList.includes(toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (!isVisible || !permission) return;
|
||||||
|
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleApproveAndReconnect();
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleDismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
{#if isVisible && permission}
|
{#if isVisible && permission}
|
||||||
<div
|
<div
|
||||||
class="permission-overlay fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm"
|
class="permission-overlay fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import StatsDisplay from "./StatsDisplay.svelte";
|
import StatsDisplay from "./StatsDisplay.svelte";
|
||||||
import AboutPanel from "./AboutPanel.svelte";
|
import AboutPanel from "./AboutPanel.svelte";
|
||||||
import HelpPanel from "./HelpPanel.svelte";
|
import HelpPanel from "./HelpPanel.svelte";
|
||||||
|
import KeyboardShortcutsModal from "./KeyboardShortcutsModal.svelte";
|
||||||
import { achievementProgress } from "$lib/stores/achievements";
|
import { achievementProgress } from "$lib/stores/achievements";
|
||||||
|
|
||||||
const DISCORD_URL = "https://chat.nhcarrigan.com";
|
const DISCORD_URL = "https://chat.nhcarrigan.com";
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
let showStats = $state(false);
|
let showStats = $state(false);
|
||||||
let showAbout = $state(false);
|
let showAbout = $state(false);
|
||||||
let showHelp = $state(false);
|
let showHelp = $state(false);
|
||||||
|
let showKeyboardShortcuts = $state(false);
|
||||||
const progress = $derived($achievementProgress);
|
const progress = $derived($achievementProgress);
|
||||||
let currentConfig: HikariConfig = $state({
|
let currentConfig: HikariConfig = $state({
|
||||||
model: null,
|
model: null,
|
||||||
@@ -272,6 +274,26 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => (showKeyboardShortcuts = true)}
|
||||||
|
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
|
||||||
|
title="Keyboard Shortcuts"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 3C10.22 3 8.47 3.23 6.86 3.68A2 2 0 005 5.57V18.43a2 2 0 001.86 1.89C8.47 20.77 10.22 21 12 21s3.53-.23 5.14-.68A2 2 0 0019 18.43V5.57a2 2 0 00-1.86-1.89C15.53 3.23 13.78 3 12 3z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 7h.01M12 7h.01M16 7h.01M8 11h.01M12 11h.01M16 11h.01M8 15h8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showHelp = true)}
|
onclick={() => (showHelp = true)}
|
||||||
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
|
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
|
||||||
@@ -341,3 +363,7 @@
|
|||||||
{#if showHelp}
|
{#if showHelp}
|
||||||
<HelpPanel onClose={() => (showHelp = false)} />
|
<HelpPanel onClose={() => (showHelp = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showKeyboardShortcuts}
|
||||||
|
<KeyboardShortcutsModal onClose={() => (showKeyboardShortcuts = false)} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { get } from "svelte/store";
|
||||||
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
|
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
|
||||||
import { configStore, applyTheme } from "$lib/stores/config";
|
import { configStore, applyTheme } from "$lib/stores/config";
|
||||||
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
||||||
import { conversationsStore } from "$lib/stores/conversations";
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
|
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import "$lib/notifications/testNotifications";
|
import "$lib/notifications/testNotifications";
|
||||||
import Terminal from "$lib/components/Terminal.svelte";
|
import Terminal from "$lib/components/Terminal.svelte";
|
||||||
@@ -18,6 +21,67 @@
|
|||||||
let initialized = false;
|
let initialized = false;
|
||||||
let achievementPanelOpen = $state(false);
|
let achievementPanelOpen = $state(false);
|
||||||
|
|
||||||
|
// Global keyboard shortcuts
|
||||||
|
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||||
|
// Don't trigger shortcuts when typing in inputs (except for specific ones)
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
|
||||||
|
|
||||||
|
// Escape closes panels (always works)
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
// Check if any panels are open and close them
|
||||||
|
if (achievementPanelOpen) {
|
||||||
|
achievementPanelOpen = false;
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ConfigSidebar handles its own escape via store
|
||||||
|
if (get(configStore.isSidebarOpen)) {
|
||||||
|
configStore.closeSidebar();
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip other shortcuts if user is typing in an input
|
||||||
|
if (isInputFocused) return;
|
||||||
|
|
||||||
|
// Ctrl+L - Clear terminal
|
||||||
|
if (event.ctrlKey && event.key === "l") {
|
||||||
|
event.preventDefault();
|
||||||
|
claudeStore.clearTerminal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+, - Open settings
|
||||||
|
if (event.ctrlKey && event.key === ",") {
|
||||||
|
event.preventDefault();
|
||||||
|
configStore.openSidebar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+C - Interrupt (only when processing)
|
||||||
|
if (event.ctrlKey && event.key === "c") {
|
||||||
|
if (get(isClaudeProcessing)) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleInterrupt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleInterrupt() {
|
||||||
|
try {
|
||||||
|
const conversationId = get(claudeStore.activeConversationId);
|
||||||
|
if (!conversationId) return;
|
||||||
|
|
||||||
|
await invoke("interrupt_claude", { conversationId });
|
||||||
|
claudeStore.addLine("system", "Process interrupted");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to interrupt:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
@@ -40,6 +104,9 @@
|
|||||||
|
|
||||||
// Initialize notification settings sync
|
// Initialize notification settings sync
|
||||||
initNotificationSync();
|
initNotificationSync();
|
||||||
|
|
||||||
|
// Add global keyboard shortcut listener
|
||||||
|
window.addEventListener("keydown", handleGlobalKeydown);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,6 +114,7 @@
|
|||||||
if (initialized) {
|
if (initialized) {
|
||||||
cleanupTauriListeners();
|
cleanupTauriListeners();
|
||||||
cleanupNotificationSync();
|
cleanupNotificationSync();
|
||||||
|
window.removeEventListener("keydown", handleGlobalKeydown);
|
||||||
initialized = false;
|
initialized = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user