feat(webui): guide queued prompt with second Enter
This commit is contained in:
@@ -852,6 +852,8 @@ export function ThreadComposer({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const chipRefs = useRef(new Map<string, HTMLButtonElement>());
|
||||
const queuedPromptCounterRef = useRef(0);
|
||||
// Only the prompt queued by the immediately preceding Enter can use the second-Enter shortcut.
|
||||
const secondEnterPromptIdRef = useRef<string | null>(null);
|
||||
const draggedQueuedPromptIdRef = useRef<string | null>(null);
|
||||
const previousPendingQueueKeyRef = useRef(pendingQueueKey);
|
||||
const wasStreamingRef = useRef(isStreaming);
|
||||
@@ -871,6 +873,7 @@ export function ThreadComposer({
|
||||
&& workspaceControls?.can_change_project !== false;
|
||||
|
||||
useEffect(() => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
skipQueuedPromptPersistRef.current = true;
|
||||
setQueuedPrompts(queuedPromptStorageKey ? readQueuedPrompts(queuedPromptStorageKey) : []);
|
||||
}, [queuedPromptStorageKey]);
|
||||
@@ -902,6 +905,7 @@ export function ThreadComposer({
|
||||
const addFiles = useCallback(
|
||||
(files: File[]) => {
|
||||
if (files.length === 0) return;
|
||||
secondEnterPromptIdRef.current = null;
|
||||
const { rejected } = enqueue(files);
|
||||
if (rejected.length > 0) {
|
||||
setInlineError(formatRejection(rejected[0].reason));
|
||||
@@ -1251,6 +1255,7 @@ export function ThreadComposer({
|
||||
useLayoutEffect(() => {
|
||||
if (previousPendingQueueKeyRef.current === pendingQueueKey) return;
|
||||
previousPendingQueueKeyRef.current = pendingQueueKey;
|
||||
secondEnterPromptIdRef.current = null;
|
||||
setValue("");
|
||||
setInlineError(null);
|
||||
setSlashMenuDismissed(false);
|
||||
@@ -1268,6 +1273,7 @@ export function ThreadComposer({
|
||||
const appendTranscription = useCallback((text: string) => {
|
||||
const transcript = text.trim();
|
||||
if (!transcript) return;
|
||||
secondEnterPromptIdRef.current = null;
|
||||
setValue((current) => {
|
||||
if (!current.trim()) return transcript;
|
||||
const separator = /[\s\n]$/.test(current) ? "" : " ";
|
||||
@@ -1405,10 +1411,12 @@ export function ThreadComposer({
|
||||
if (!canQueueGuidance || (!text && readyImages.length === 0)) return;
|
||||
const queuedImages = readyImagesToQueuedImages(readyImages);
|
||||
queuedPromptCounterRef.current += 1;
|
||||
const id = `queued-prompt-${Date.now()}-${queuedPromptCounterRef.current}`;
|
||||
secondEnterPromptIdRef.current = id;
|
||||
setQueuedPrompts((items) => [
|
||||
...items,
|
||||
{
|
||||
id: `queued-prompt-${Date.now()}-${queuedPromptCounterRef.current}`,
|
||||
id,
|
||||
text,
|
||||
...(queuedImages.length > 0 ? { images: queuedImages } : {}),
|
||||
},
|
||||
@@ -1418,11 +1426,13 @@ export function ThreadComposer({
|
||||
}, [canQueueGuidance, clear, clearComposerText, readyImages, value]);
|
||||
|
||||
const removeQueuedPrompt = useCallback((id: string) => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
setQueuedPrompts((items) => items.filter((item) => item.id !== id));
|
||||
requestAnimationFrame(() => textareaRef.current?.focus());
|
||||
}, []);
|
||||
|
||||
const editQueuedPrompt = useCallback((prompt: QueuedPrompt) => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
setQueuedPrompts((items) => items.filter((item) => item.id !== prompt.id));
|
||||
setValue(prompt.text);
|
||||
setInlineError(null);
|
||||
@@ -1445,6 +1455,7 @@ export function ThreadComposer({
|
||||
|
||||
const moveQueuedPrompt = useCallback((dragId: string, targetId: string) => {
|
||||
if (dragId === targetId) return;
|
||||
secondEnterPromptIdRef.current = null;
|
||||
setQueuedPrompts((items) => {
|
||||
const from = items.findIndex((item) => item.id === dragId);
|
||||
const to = items.findIndex((item) => item.id === targetId);
|
||||
@@ -1458,6 +1469,7 @@ export function ThreadComposer({
|
||||
|
||||
const sendQueuedPrompt = useCallback(
|
||||
(prompt: QueuedPrompt) => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
const text = prompt.text.trim();
|
||||
const queuedImages = queuedImagesToSendImages(prompt.images);
|
||||
setQueuedPrompts((items) => items.filter((item) => item.id !== prompt.id));
|
||||
@@ -1487,6 +1499,7 @@ export function ThreadComposer({
|
||||
useEffect(() => {
|
||||
const wasStreaming = wasStreamingRef.current;
|
||||
wasStreamingRef.current = isStreaming;
|
||||
if (!isStreaming) secondEnterPromptIdRef.current = null;
|
||||
if (!wasStreaming || isStreaming || queuedPrompts.length === 0) return;
|
||||
if (skipNextQueuedFlushRef.current) {
|
||||
skipNextQueuedFlushRef.current = false;
|
||||
@@ -1496,6 +1509,7 @@ export function ThreadComposer({
|
||||
}, [sendNextQueuedPrompt, isStreaming, queuedPrompts.length]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
if (queuedPrompts.length > 0) {
|
||||
skipNextQueuedFlushRef.current = true;
|
||||
}
|
||||
@@ -1639,9 +1653,25 @@ export function ThreadComposer({
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||
e.preventDefault();
|
||||
if (canQueueGuidance) {
|
||||
queueGuidancePrompt();
|
||||
if (!e.repeat) queueGuidancePrompt();
|
||||
return;
|
||||
}
|
||||
const secondEnterPrompt = queuedPrompts.find(
|
||||
(prompt) => prompt.id === secondEnterPromptIdRef.current,
|
||||
);
|
||||
if (
|
||||
isStreaming
|
||||
&& value.length === 0
|
||||
&& images.length === 0
|
||||
&& !e.altKey
|
||||
&& !e.ctrlKey
|
||||
&& !e.metaKey
|
||||
&& secondEnterPrompt
|
||||
) {
|
||||
if (!e.repeat) sendQueuedPrompt(secondEnterPrompt);
|
||||
return;
|
||||
}
|
||||
secondEnterPromptIdRef.current = null;
|
||||
submit();
|
||||
}
|
||||
};
|
||||
@@ -1779,6 +1809,7 @@ export function ThreadComposer({
|
||||
onDelete={removeQueuedPrompt}
|
||||
onEdit={editQueuedPrompt}
|
||||
onDragStart={(id) => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
draggedQueuedPromptIdRef.current = id;
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
@@ -1831,11 +1862,15 @@ export function ThreadComposer({
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
setValue(e.target.value);
|
||||
setSlashMenuDismissed(false);
|
||||
setCliAppMenuDismissed(false);
|
||||
setCursorPosition(e.target.selectionStart ?? e.target.value.length);
|
||||
}}
|
||||
onBlur={() => {
|
||||
secondEnterPromptIdRef.current = null;
|
||||
}}
|
||||
onInput={onInput}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={(e) => setCursorPosition(e.currentTarget.selectionStart ?? e.currentTarget.value.length)}
|
||||
|
||||
Reference in New Issue
Block a user