enable prompt shell mode
This commit is contained in:
@@ -16,6 +16,7 @@ interface PromptInputProps {
|
||||
instanceFolder: string
|
||||
sessionId: string
|
||||
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
|
||||
onRunShell?: (command: string) => Promise<void>
|
||||
disabled?: boolean
|
||||
escapeInDebounce?: boolean
|
||||
}
|
||||
@@ -33,6 +34,7 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set<number>())
|
||||
const [pasteCount, setPasteCount] = createSignal(0)
|
||||
const [imageCount, setImageCount] = createSignal(0)
|
||||
const [mode, setMode] = createSignal<"normal" | "shell">("normal")
|
||||
let textareaRef: HTMLTextAreaElement | undefined
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
|
||||
@@ -51,6 +53,7 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
clearSessionDraftPrompt(props.instanceId, props.sessionId)
|
||||
setPromptInternal("")
|
||||
setHistoryDraft(null)
|
||||
setMode("normal")
|
||||
}
|
||||
|
||||
function syncAttachmentCounters(currentPrompt: string, sessionAttachments: Attachment[]) {
|
||||
@@ -295,9 +298,40 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentText = prompt()
|
||||
const cursorAtBufferStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
||||
const isShellMode = mode() === "shell"
|
||||
|
||||
if (!isShellMode && e.key === "!" && cursorAtBufferStart && currentText.length === 0 && !props.disabled) {
|
||||
e.preventDefault()
|
||||
setMode("shell")
|
||||
return
|
||||
}
|
||||
|
||||
if (showPicker() && e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handlePickerClose()
|
||||
return
|
||||
}
|
||||
|
||||
if (isShellMode) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setMode("normal")
|
||||
return
|
||||
}
|
||||
if (e.key === "Backspace" && cursorAtBufferStart && currentText.length === 0) {
|
||||
e.preventDefault()
|
||||
setMode("normal")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Backspace" || e.key === "Delete") {
|
||||
const cursorPos = textarea.selectionStart
|
||||
const text = prompt()
|
||||
const text = currentText
|
||||
|
||||
const pastePlaceholderRegex = /\[pasted #(\d+)\]/g
|
||||
let pasteMatch
|
||||
@@ -464,9 +498,10 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
async function handleSend() {
|
||||
const text = prompt().trim()
|
||||
const currentAttachments = attachments()
|
||||
if (!text || props.disabled) return
|
||||
if (props.disabled || !text) return
|
||||
|
||||
const resolvedPrompt = resolvePastedPlaceholders(text, currentAttachments)
|
||||
const isShellMode = mode() === "shell"
|
||||
|
||||
clearPrompt()
|
||||
clearAttachments(props.instanceId, props.sessionId)
|
||||
@@ -480,7 +515,15 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
const updated = await getHistory(props.instanceFolder)
|
||||
setHistory(updated)
|
||||
setHistoryIndex(-1)
|
||||
await props.onSend(text, currentAttachments)
|
||||
if (isShellMode) {
|
||||
if (props.onRunShell) {
|
||||
await props.onRunShell(resolvedPrompt)
|
||||
} else {
|
||||
await props.onSend(resolvedPrompt, [])
|
||||
}
|
||||
} else {
|
||||
await props.onSend(text, currentAttachments)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error)
|
||||
alert("Failed to send message: " + (error instanceof Error ? error.message : String(error)))
|
||||
@@ -667,7 +710,14 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
textareaRef?.focus()
|
||||
}
|
||||
|
||||
const canSend = () => (prompt().trim().length > 0 || attachments().length > 0) && !props.disabled
|
||||
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 instance = () => getActiveInstance()
|
||||
|
||||
@@ -676,7 +726,11 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`prompt-input-wrapper relative ${isDragging() ? "border-2" : ""}`}
|
||||
style={isDragging() ? "border-color: var(--accent-primary); background-color: rgba(0, 102, 255, 0.05);" : ""}
|
||||
style={
|
||||
isDragging()
|
||||
? "border-color: var(--accent-primary); background-color: rgba(0, 102, 255, 0.05);"
|
||||
: ""
|
||||
}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
@@ -768,8 +822,12 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
</Show>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
class="prompt-input"
|
||||
placeholder="Type your message, @file, @agent, or paste images and text..."
|
||||
class={`prompt-input ${mode() === "shell" ? "shell-mode" : ""}`}
|
||||
placeholder={
|
||||
mode() === "shell"
|
||||
? "Run a shell command (Esc to exit)..."
|
||||
: "Type your message, @file, @agent, or paste images and text..."
|
||||
}
|
||||
value={prompt()}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -786,22 +844,37 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="send-button" onClick={handleSend} disabled={!canSend()} aria-label="Send message">
|
||||
<span class="send-icon">▶</span>
|
||||
<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 class="prompt-input-hints">
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-between w-full gap-4">
|
||||
<HintRow>
|
||||
<Show
|
||||
when={props.escapeInDebounce}
|
||||
fallback={
|
||||
<>
|
||||
<Kbd>Enter</Kbd> for new line • <Kbd shortcut="cmd+enter" /> to send • <Kbd>@</Kbd> for files/agents •{" "}
|
||||
<Kbd>↑↓</Kbd> for history
|
||||
<Kbd>Enter</Kbd> for new line • <Kbd shortcut="cmd+enter" /> to send • <Kbd>@</Kbd> for files/agents • <Kbd>↑↓</Kbd> for history
|
||||
<Show when={attachments().length > 0}>
|
||||
<span class="ml-2 text-xs" style="color: var(--text-muted);">• {attachments().length} file(s) attached</span>
|
||||
</Show>
|
||||
<span class="ml-2">
|
||||
• <Kbd>{shellHint().key}</Kbd> {shellHint().text}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -810,6 +883,9 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
</span>
|
||||
</Show>
|
||||
</HintRow>
|
||||
<Show when={mode() === "shell"}>
|
||||
<HintRow>Shell mode active</HintRow>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ClientPart } from "../../types/message"
|
||||
import MessageStream from "../message-stream"
|
||||
import PromptInput from "../prompt-input"
|
||||
import { instances } from "../../stores/instances"
|
||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession } from "../../stores/sessions"
|
||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand } from "../../stores/sessions"
|
||||
|
||||
interface SessionViewProps {
|
||||
sessionId: string
|
||||
@@ -30,6 +30,10 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
||||
}
|
||||
|
||||
async function handleRunShell(command: string) {
|
||||
await runShellCommand(props.instanceId, props.sessionId, command)
|
||||
}
|
||||
|
||||
function getUserMessageText(messageId: string): string | null {
|
||||
const currentSession = session()
|
||||
if (!currentSession) return null
|
||||
@@ -134,6 +138,7 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
instanceFolder={props.instanceFolder}
|
||||
sessionId={s().id}
|
||||
onSend={handleSendMessage}
|
||||
onRunShell={handleRunShell}
|
||||
escapeInDebounce={props.escapeInDebounce}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
preferences,
|
||||
setAgentModelPreference,
|
||||
} from "./preferences"
|
||||
import {
|
||||
sessions,
|
||||
withSession,
|
||||
} from "./session-state"
|
||||
import { sessions, withSession } from "./session-state"
|
||||
import { getDefaultModel, isModelValid } from "./session-models"
|
||||
import {
|
||||
computeDisplayParts,
|
||||
@@ -249,6 +246,28 @@ async function executeCustomCommand(
|
||||
})
|
||||
}
|
||||
|
||||
async function runShellCommand(instanceId: string, sessionId: string, command: string): Promise<void> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
const session = sessions().get(instanceId)?.get(sessionId)
|
||||
if (!session) {
|
||||
throw new Error("Session not found")
|
||||
}
|
||||
|
||||
const agent = session.agent || "build"
|
||||
|
||||
await instance.client.session.shell({
|
||||
path: { id: sessionId },
|
||||
body: {
|
||||
agent,
|
||||
command,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function abortSession(instanceId: string, sessionId: string): Promise<void> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
@@ -326,6 +345,7 @@ async function updateSessionModel(
|
||||
export {
|
||||
abortSession,
|
||||
executeCustomCommand,
|
||||
runShellCommand,
|
||||
sendMessage,
|
||||
updateSessionAgent,
|
||||
updateSessionModel,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import {
|
||||
abortSession,
|
||||
executeCustomCommand,
|
||||
runShellCommand,
|
||||
sendMessage,
|
||||
updateSessionAgent,
|
||||
updateSessionModel,
|
||||
@@ -82,6 +83,7 @@ export {
|
||||
createSession,
|
||||
deleteSession,
|
||||
executeCustomCommand,
|
||||
runShellCommand,
|
||||
fetchAgents,
|
||||
fetchProviders,
|
||||
fetchSessions,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@apply flex items-end gap-2 p-3;
|
||||
}
|
||||
|
||||
|
||||
.prompt-input {
|
||||
@apply flex-1 min-h-[96px] max-h-[200px] p-2.5 border rounded-md text-sm resize-none outline-none transition-colors;
|
||||
font-family: inherit;
|
||||
@@ -18,10 +19,20 @@
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.prompt-input.shell-mode {
|
||||
border-color: var(--status-success);
|
||||
box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.prompt-input:focus {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.prompt-input.shell-mode:focus {
|
||||
border-color: var(--status-success);
|
||||
box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.prompt-input:disabled {
|
||||
@apply opacity-60 cursor-not-allowed;
|
||||
}
|
||||
@@ -36,6 +47,18 @@
|
||||
color: var(--text-inverted);
|
||||
}
|
||||
|
||||
.send-button.shell-mode {
|
||||
background-color: var(--status-success);
|
||||
}
|
||||
|
||||
.send-button.shell-mode:hover:not(:disabled) {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.send-button.shell-mode:active:not(:disabled) {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.send-button:hover:not(:disabled) {
|
||||
@apply opacity-90 scale-105;
|
||||
}
|
||||
@@ -52,6 +75,11 @@
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
.shell-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.prompt-input-hints {
|
||||
@apply px-4 pb-2 flex justify-between items-center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user