add stop control to prompt input
This commit is contained in:
@@ -22,6 +22,8 @@ interface PromptInputProps {
|
||||
onRunShell?: (command: string) => Promise<void>
|
||||
disabled?: boolean
|
||||
escapeInDebounce?: boolean
|
||||
isSessionBusy?: boolean
|
||||
onAbortSession?: () => Promise<void>
|
||||
}
|
||||
|
||||
export default function PromptInput(props: PromptInputProps) {
|
||||
@@ -599,8 +601,14 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
textareaRef?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleAbort() {
|
||||
if (!props.onAbortSession || !props.isSessionBusy) return
|
||||
void props.onAbortSession()
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
const value = target.value
|
||||
setPrompt(value)
|
||||
@@ -818,14 +826,17 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
textareaRef?.focus()
|
||||
}
|
||||
|
||||
const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession)
|
||||
|
||||
const canSend = () => {
|
||||
if (props.disabled) return false
|
||||
const hasText = prompt().trim().length > 0
|
||||
if (mode() === "shell") return hasText
|
||||
return hasText || attachments().length > 0
|
||||
}
|
||||
|
||||
|
||||
const shellHint = () => (mode() === "shell" ? { key: "Esc", text: "to exit shell mode" } : { key: "!", text: "for shell mode" })
|
||||
|
||||
const shouldShowOverlay = () => prompt().length === 0
|
||||
|
||||
const instance = () => getActiveInstance()
|
||||
@@ -1010,22 +1021,37 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class={`send-button ${mode() === "shell" ? "shell-mode" : ""}`}
|
||||
onClick={handleSend}
|
||||
disabled={!canSend()}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Show
|
||||
when={mode() === "shell"}
|
||||
fallback={<span class="send-icon">▶</span>}
|
||||
<div class="prompt-input-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="stop-button"
|
||||
onClick={handleAbort}
|
||||
disabled={!canStop()}
|
||||
aria-label="Stop session"
|
||||
title="Stop session"
|
||||
>
|
||||
<svg class="shell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 8l5 4-5 4" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h6" />
|
||||
<svg class="stop-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<rect x="4" y="4" width="12" height="12" rx="2" />
|
||||
</svg>
|
||||
</Show>
|
||||
</button>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`send-button ${mode() === "shell" ? "shell-mode" : ""}`}
|
||||
onClick={handleSend}
|
||||
disabled={!canSend()}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Show
|
||||
when={mode() === "shell"}
|
||||
fallback={<span class="send-icon">▶</span>}
|
||||
>
|
||||
<svg class="shell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 8l5 4-5 4" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h6" />
|
||||
</svg>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,8 @@ import MessageSection from "../message-section"
|
||||
import { messageStoreBus } from "../../stores/message-v2/bus"
|
||||
import PromptInput from "../prompt-input"
|
||||
import { instances } from "../../stores/instances"
|
||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand } from "../../stores/sessions"
|
||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
||||
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
|
||||
import { showAlertDialog } from "../../stores/alerts"
|
||||
import { getLogger } from "../../lib/logger"
|
||||
|
||||
@@ -31,17 +32,22 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
const session = () => props.activeSessions.get(props.sessionId)
|
||||
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
|
||||
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
||||
const sessionBusy = createMemo(() => {
|
||||
const currentSession = session()
|
||||
if (!currentSession) return false
|
||||
return getSessionBusyStatus(props.instanceId, currentSession.id)
|
||||
})
|
||||
let scrollToBottomHandle: (() => void) | undefined
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
createEffect(() => {
|
||||
const currentSession = session()
|
||||
if (currentSession) {
|
||||
loadMessages(props.instanceId, currentSession.id).catch((error) => log.error("Failed to load messages", error))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
|
||||
|
||||
if (scrollToBottomHandle) {
|
||||
scrollToBottomHandle()
|
||||
}
|
||||
@@ -51,8 +57,26 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
async function handleRunShell(command: string) {
|
||||
await runShellCommand(props.instanceId, props.sessionId, command)
|
||||
}
|
||||
|
||||
|
||||
async function handleAbortSession() {
|
||||
const currentSession = session()
|
||||
if (!currentSession) return
|
||||
|
||||
try {
|
||||
await abortSession(props.instanceId, currentSession.id)
|
||||
log.info("Abort requested", { instanceId: props.instanceId, sessionId: currentSession.id })
|
||||
} catch (error) {
|
||||
log.error("Failed to abort session", error)
|
||||
showAlertDialog("Failed to stop session", {
|
||||
title: "Stop failed",
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
variant: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getUserMessageText(messageId: string): string | null {
|
||||
|
||||
const normalizedMessage = messageStore().getMessage(messageId)
|
||||
if (normalizedMessage && normalizedMessage.role === "user") {
|
||||
const parts = normalizedMessage.partIds
|
||||
@@ -169,6 +193,8 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
onSend={handleSendMessage}
|
||||
onRunShell={handleRunShell}
|
||||
escapeInDebounce={props.escapeInDebounce}
|
||||
isSessionBusy={sessionBusy()}
|
||||
onAbortSession={handleAbortSession}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
}
|
||||
|
||||
.prompt-input-wrapper {
|
||||
@apply flex items-end gap-2 p-3;
|
||||
@apply flex items-stretch gap-2 p-2;
|
||||
}
|
||||
|
||||
.prompt-input-actions {
|
||||
@apply flex flex-col items-center justify-between;
|
||||
align-self: stretch;
|
||||
height: 100%;
|
||||
padding: 0.25rem 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.prompt-input-field {
|
||||
@@ -14,14 +22,15 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
@apply flex-1 w-full min-h-[56px] max-h-[96px] px-3 pt-2.5 pb-12 border rounded-md text-sm resize-none outline-none transition-colors;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface-base);
|
||||
color: inherit;
|
||||
border-color: var(--border-base);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
.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;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface-base);
|
||||
color: inherit;
|
||||
border-color: var(--border-base);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
|
||||
.prompt-input-overlay {
|
||||
position: absolute;
|
||||
@@ -84,6 +93,31 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stop-button {
|
||||
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
||||
background-color: rgba(239, 68, 68, 0.85);
|
||||
color: var(--text-inverted);
|
||||
}
|
||||
|
||||
.stop-button:hover:not(:disabled) {
|
||||
background-color: rgba(239, 68, 68, 0.9);
|
||||
@apply opacity-95 scale-105;
|
||||
}
|
||||
|
||||
.stop-button:active:not(:disabled) {
|
||||
background-color: rgba(239, 68, 68, 1);
|
||||
@apply scale-95;
|
||||
}
|
||||
|
||||
.stop-button:disabled {
|
||||
@apply opacity-60 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.stop-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
@apply w-10 h-10 rounded-md border-none cursor-pointer flex items-center justify-center transition-all flex-shrink-0;
|
||||
background-color: var(--accent-primary);
|
||||
|
||||
Reference in New Issue
Block a user