add prompt history rocker

This commit is contained in:
Shantur Rathore
2025-12-06 23:13:01 +00:00
parent de432106e5
commit b9394fb467
2 changed files with 133 additions and 23 deletions

View File

@@ -519,31 +519,19 @@ export default function PromptInput(props: PromptInputProps) {
return
}
const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0
const currentHistory = history()
if (e.key === "ArrowUp" && !showPicker() && atStart && currentHistory.length > 0) {
e.preventDefault()
if (historyIndex() === -1) {
setHistoryDraft(prompt())
if (e.key === "ArrowUp") {
const handled = selectPreviousHistory()
if (handled) {
e.preventDefault()
return
}
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, currentHistory.length - 1)
setHistoryIndex(newIndex)
setPrompt(currentHistory[newIndex])
return
}
if (e.key === "ArrowDown" && !showPicker() && historyIndex() >= 0) {
e.preventDefault()
const newIndex = historyIndex() - 1
if (newIndex >= 0) {
setHistoryIndex(newIndex)
setPrompt(currentHistory[newIndex])
} else {
setHistoryIndex(-1)
const draft = historyDraft()
setPrompt(draft ?? "")
setHistoryDraft(null)
if (e.key === "ArrowDown") {
const handled = selectNextHistory()
if (handled) {
e.preventDefault()
return
}
}
}
@@ -602,6 +590,60 @@ export default function PromptInput(props: PromptInputProps) {
}
}
function focusTextareaEnd() {
if (!textareaRef) return
setTimeout(() => {
if (!textareaRef) return
const pos = textareaRef.value.length
textareaRef.setSelectionRange(pos, pos)
textareaRef.focus()
}, 0)
}
function canUseHistory(force = false) {
if (force) return true
if (showPicker()) return false
const textarea = textareaRef
if (!textarea) return false
return textarea.selectionStart === 0 && textarea.selectionEnd === 0
}
function selectPreviousHistory(force = false) {
const entries = history()
if (entries.length === 0) return false
if (!canUseHistory(force)) return false
if (historyIndex() === -1) {
setHistoryDraft(prompt())
}
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, entries.length - 1)
setHistoryIndex(newIndex)
setPrompt(entries[newIndex])
focusTextareaEnd()
return true
}
function selectNextHistory(force = false) {
const entries = history()
if (entries.length === 0) return false
if (!canUseHistory(force)) return false
if (historyIndex() === -1) return false
const newIndex = historyIndex() - 1
if (newIndex >= 0) {
setHistoryIndex(newIndex)
setPrompt(entries[newIndex])
} else {
setHistoryIndex(-1)
const draft = historyDraft()
setPrompt(draft ?? "")
setHistoryDraft(null)
}
focusTextareaEnd()
return true
}
function handleAbort() {
if (!props.onAbortSession || !props.isSessionBusy) return
void props.onAbortSession()
@@ -828,6 +870,10 @@ export default function PromptInput(props: PromptInputProps) {
const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession)
const hasHistory = () => history().length > 0
const canHistoryGoPrevious = () => hasHistory() && (historyIndex() === -1 || historyIndex() < history().length - 1)
const canHistoryGoNext = () => historyIndex() >= 0
const canSend = () => {
if (props.disabled) return false
const hasText = prompt().trim().length > 0
@@ -986,6 +1032,34 @@ export default function PromptInput(props: PromptInputProps) {
autoCapitalize="off"
autocomplete="off"
/>
<Show when={hasHistory()}>
<div class="prompt-history-top">
<button
type="button"
class="prompt-history-button"
onClick={() => selectPreviousHistory(true)}
disabled={!canHistoryGoPrevious()}
aria-label="Previous prompt"
>
<svg viewBox="0 0 20 20" fill="currentColor" class="h-3 w-3" aria-hidden="true">
<path d="M10 6l-4 5h8L10 6z" />
</svg>
</button>
</div>
<div class="prompt-history-bottom">
<button
type="button"
class="prompt-history-button"
onClick={() => selectNextHistory(true)}
disabled={!canHistoryGoNext()}
aria-label="Next prompt"
>
<svg viewBox="0 0 20 20" fill="currentColor" class="h-3 w-3" aria-hidden="true">
<path d="M10 14l4-5H6l4 5z" />
</svg>
</button>
</div>
</Show>
<Show when={shouldShowOverlay()}>
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
<Show

View File

@@ -23,7 +23,7 @@
}
.prompt-input {
@apply flex-1 w-full min-h-[56px] max-h-[96px] px-3 pt-2.5 pb-4 border rounded-md text-sm resize-none outline-none transition-colors;
@apply flex-1 w-full min-h-[56px] max-h-[96px] pl-3 pr-10 pt-2.5 pb-4 border rounded-md text-sm resize-none outline-none transition-colors;
font-family: inherit;
background-color: var(--surface-base);
color: inherit;
@@ -51,6 +51,42 @@
color: var(--text-primary);
}
.prompt-history-top,
.prompt-history-bottom {
position: absolute;
right: 0.35rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.prompt-history-top {
top: 0.3rem;
}
.prompt-history-bottom {
bottom: 0.3rem;
}
.prompt-history-button {
@apply w-8 h-8 flex items-center justify-center rounded-md;
color: var(--text-muted);
background-color: rgba(15, 23, 42, 0.04);
transition: background-color 0.15s ease, color 0.15s ease;
padding: 0;
}
.prompt-history-button:hover:not(:disabled) {
background-color: var(--surface-secondary);
color: var(--text-primary);
}
.prompt-history-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.prompt-overlay-text {
display: inline-flex;
align-items: center;