add prompt history rocker
This commit is contained in:
@@ -519,31 +519,19 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
if (e.key === "ArrowUp") {
|
||||||
const currentHistory = history()
|
const handled = selectPreviousHistory()
|
||||||
|
if (handled) {
|
||||||
if (e.key === "ArrowUp" && !showPicker() && atStart && currentHistory.length > 0) {
|
e.preventDefault()
|
||||||
e.preventDefault()
|
return
|
||||||
if (historyIndex() === -1) {
|
|
||||||
setHistoryDraft(prompt())
|
|
||||||
}
|
}
|
||||||
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) {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault()
|
const handled = selectNextHistory()
|
||||||
const newIndex = historyIndex() - 1
|
if (handled) {
|
||||||
if (newIndex >= 0) {
|
e.preventDefault()
|
||||||
setHistoryIndex(newIndex)
|
return
|
||||||
setPrompt(currentHistory[newIndex])
|
|
||||||
} else {
|
|
||||||
setHistoryIndex(-1)
|
|
||||||
const draft = historyDraft()
|
|
||||||
setPrompt(draft ?? "")
|
|
||||||
setHistoryDraft(null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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() {
|
function handleAbort() {
|
||||||
if (!props.onAbortSession || !props.isSessionBusy) return
|
if (!props.onAbortSession || !props.isSessionBusy) return
|
||||||
void props.onAbortSession()
|
void props.onAbortSession()
|
||||||
@@ -828,6 +870,10 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
|
|
||||||
const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession)
|
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 = () => {
|
const canSend = () => {
|
||||||
if (props.disabled) return false
|
if (props.disabled) return false
|
||||||
const hasText = prompt().trim().length > 0
|
const hasText = prompt().trim().length > 0
|
||||||
@@ -986,6 +1032,34 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autocomplete="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()}>
|
<Show when={shouldShowOverlay()}>
|
||||||
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
|
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input {
|
.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;
|
font-family: inherit;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-base);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -51,6 +51,42 @@
|
|||||||
color: var(--text-primary);
|
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 {
|
.prompt-overlay-text {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user