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