Add keyboard shortcuts system with reusable hint components

- Implement centralized keyboard registry with 20+ shortcuts
- Add instance navigation (Cmd+[1-9], Cmd+[/])
- Add session navigation (Cmd+Shift+[1-9], Cmd+Shift+[/])
- Add agent/model cycling (Tab, Cmd+Shift+M)
- Add input shortcuts (Cmd+P focus, Cmd+K clear, ↑↓ history)
- Add command palette (Cmd+Shift+P) with 8 MVP commands
- Implement message history per folder in IndexedDB (max 100)
- Create reusable Kbd and HintRow components
- Replace all keyboard hint rendering with consistent components
- Use text-based shortcuts (Cmd+Shift+M) for clarity
This commit is contained in:
Shantur Rathore
2025-10-23 20:18:45 +01:00
parent 3c5c4755b8
commit 4c98a3df06
21 changed files with 1302 additions and 143 deletions

View File

@@ -1,9 +1,14 @@
import { createSignal, Show } from "solid-js"
import { createSignal, Show, onMount, createEffect } from "solid-js"
import AgentSelector from "./agent-selector"
import ModelSelector from "./model-selector"
import { addToHistory, getHistory } from "../stores/message-history"
import Kbd from "./kbd"
import HintRow from "./hint-row"
import { isMac } from "../lib/keyboard-utils"
interface PromptInputProps {
instanceId: string
instanceFolder: string
sessionId: string
onSend: (prompt: string) => Promise<void>
disabled?: boolean
@@ -16,12 +21,56 @@ interface PromptInputProps {
export default function PromptInput(props: PromptInputProps) {
const [prompt, setPrompt] = createSignal("")
const [sending, setSending] = createSignal(false)
const [history, setHistory] = createSignal<string[]>([])
const [historyIndex, setHistoryIndex] = createSignal(-1)
const [isFocused, setIsFocused] = createSignal(false)
let textareaRef: HTMLTextAreaElement | undefined
onMount(async () => {
const loaded = await getHistory(props.instanceFolder)
setHistory(loaded)
})
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSend()
return
}
const textarea = textareaRef
if (!textarea) return
const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0
const currentHistory = history()
if (e.key === "ArrowUp" && atStart && currentHistory.length > 0) {
e.preventDefault()
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, currentHistory.length - 1)
setHistoryIndex(newIndex)
setPrompt(currentHistory[newIndex])
setTimeout(() => {
textarea.style.height = "auto"
textarea.style.height = Math.min(textarea.scrollHeight, 200) + "px"
}, 0)
return
}
if (e.key === "ArrowDown" && historyIndex() >= 0) {
e.preventDefault()
const newIndex = historyIndex() - 1
if (newIndex >= 0) {
setHistoryIndex(newIndex)
setPrompt(currentHistory[newIndex])
} else {
setHistoryIndex(-1)
setPrompt("")
}
setTimeout(() => {
textarea.style.height = "auto"
textarea.style.height = Math.min(textarea.scrollHeight, 200) + "px"
}, 0)
return
}
}
@@ -31,6 +80,12 @@ export default function PromptInput(props: PromptInputProps) {
setSending(true)
try {
await addToHistory(props.instanceFolder, text)
const updated = await getHistory(props.instanceFolder)
setHistory(updated)
setHistoryIndex(-1)
await props.onSend(text)
setPrompt("")
@@ -49,6 +104,7 @@ export default function PromptInput(props: PromptInputProps) {
function handleInput(e: Event) {
const target = e.target as HTMLTextAreaElement
setPrompt(target.value)
setHistoryIndex(-1)
target.style.height = "auto"
target.style.height = Math.min(target.scrollHeight, 200) + "px"
@@ -66,6 +122,8 @@ export default function PromptInput(props: PromptInputProps) {
value={prompt()}
onInput={handleInput}
onKeyDown={handleKeyDown}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
disabled={sending() || props.disabled}
rows={1}
/>
@@ -76,9 +134,10 @@ export default function PromptInput(props: PromptInputProps) {
</button>
</div>
<div class="prompt-input-hints">
<span class="hint">
<kbd>Enter</kbd> to send, <kbd>Shift+Enter</kbd> for new line
</span>
<HintRow>
<Kbd>Enter</Kbd> to send <Kbd>Shift+Enter</Kbd> for new line <Kbd></Kbd> for history {" "}
<Kbd shortcut="cmd+p" /> to focus
</HintRow>
<div class="flex items-center gap-2">
<AgentSelector
instanceId={props.instanceId}