diff options
| author | Adam <[email protected]> | 2025-12-18 09:38:35 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-18 11:16:33 -0600 |
| commit | 268f37f8c9813b3805e508d37c20fec6e6e12cb1 (patch) | |
| tree | 50c24c2763e1a69cceef19f6e675d0bb6e17bca9 /packages/desktop/src/components | |
| parent | b0aaf04957336538dc49d2b0416f44dfbd538531 (diff) | |
| download | opencode-268f37f8c9813b3805e508d37c20fec6e6e12cb1.tar.gz opencode-268f37f8c9813b3805e508d37c20fec6e6e12cb1.zip | |
fix(desktop): prompt history nav, optimistic prompt dup
Diffstat (limited to 'packages/desktop/src/components')
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 87 | ||||
| -rw-r--r-- | packages/desktop/src/components/terminal.tsx | 1 |
2 files changed, 63 insertions, 25 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 02fa700bf..98092f5d5 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -21,6 +21,7 @@ import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid import { useProviders } from "@/hooks/use-providers" import { useCommand, formatKeybind } from "@/context/command" import { persisted } from "@/utils/persist" +import { Identifier } from "@opencode-ai/util/identifier" const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] @@ -100,6 +101,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { dragging: boolean imageAttachments: ImageAttachmentPart[] mode: "normal" | "shell" + applyingHistory: boolean }>({ popover: null, historyIndex: -1, @@ -108,6 +110,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { dragging: false, imageAttachments: [], mode: "normal", + applyingHistory: false, }) const MAX_HISTORY = 100 @@ -135,10 +138,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => { const length = position === "start" ? 0 : promptLength(p) + setStore("applyingHistory", true) prompt.set(p, length) requestAnimationFrame(() => { editorRef.focus() setCursorPosition(editorRef, length) + setStore("applyingHistory", false) }) } @@ -429,21 +434,42 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const rawParts = parseFromDOM() const cursorPosition = getCursorPosition(editorRef) const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("") + const trimmed = rawText.replace(/\u200B/g, "").trim() + const hasNonText = rawParts.some((part) => part.type !== "text") + const shouldReset = trimmed.length === 0 && !hasNonText - const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/) - const slashMatch = rawText.match(/^\/(\S*)$/) + if (shouldReset) { + setStore("popover", null) + if (store.historyIndex >= 0 && !store.applyingHistory) { + setStore("historyIndex", -1) + setStore("savedPrompt", null) + } + if (prompt.dirty()) { + prompt.set(DEFAULT_PROMPT, 0) + } + return + } - if (atMatch) { - onInput(atMatch[1]) - setStore("popover", "file") - } else if (slashMatch) { - slashOnInput(slashMatch[1]) - setStore("popover", "slash") + const shellMode = store.mode === "shell" + + if (!shellMode) { + const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/) + const slashMatch = rawText.match(/^\/(\S*)$/) + + if (atMatch) { + onInput(atMatch[1]) + setStore("popover", "file") + } else if (slashMatch) { + slashOnInput(slashMatch[1]) + setStore("popover", "slash") + } else { + setStore("popover", null) + } } else { setStore("popover", null) } - if (store.historyIndex >= 0) { + if (store.historyIndex >= 0 && !store.applyingHistory) { setStore("historyIndex", -1) setStore("savedPrompt", null) } @@ -591,8 +617,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } } if (store.mode === "shell") { - const cursorPosition = getCursorPosition(editorRef) - if ((event.key === "Backspace" && cursorPosition === 0) || event.key === "Escape") { + const { collapsed, cursorPosition, textLength } = getCaretState() + if (event.key === "Escape") { + setStore("mode", "normal") + event.preventDefault() + return + } + if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { setStore("mode", "normal") event.preventDefault() return @@ -685,6 +716,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { ? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}` : "" return { + id: Identifier.ascending("part"), type: "file" as const, mime: "text/plain", url: `file://${absolute}${query}`, @@ -702,6 +734,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { }) const imageAttachmentParts = store.imageAttachments.map((attachment) => ({ + id: Identifier.ascending("part"), type: "file" as const, mime: attachment.mime, url: attachment.dataUrl, @@ -747,14 +780,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } } + const messageID = Identifier.ascending("message") + const textPart = { + id: Identifier.ascending("part"), + type: "text" as const, + text, + } + const requestParts = [textPart, ...fileAttachmentParts, ...imageAttachmentParts] + const optimisticParts = requestParts.map((part) => ({ + ...part, + sessionID: existing.id, + messageID, + })) + sync.session.addOptimisticMessage({ sessionID: existing.id, - text, - parts: [ - { type: "text", text } as import("@opencode-ai/sdk/v2/client").Part, - ...(fileAttachmentParts as import("@opencode-ai/sdk/v2/client").Part[]), - ...(imageAttachmentParts as import("@opencode-ai/sdk/v2/client").Part[]), - ], + messageID, + parts: optimisticParts, agent, model, }) @@ -763,14 +805,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => { sessionID: existing.id, agent, model, - parts: [ - { - type: "text", - text, - }, - ...fileAttachmentParts, - ...imageAttachmentParts, - ], + messageID, + parts: requestParts, }) } @@ -911,6 +947,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { classList={{ "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&>[data-type=file]]:text-icon-info-active": true, + "font-mono!": store.mode === "shell", }} /> <Show when={!prompt.dirty() && store.imageAttachments.length === 0}> diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx index 2156b10ee..c05ddfbf6 100644 --- a/packages/desktop/src/components/terminal.tsx +++ b/packages/desktop/src/components/terminal.tsx @@ -148,6 +148,7 @@ export const Terminal = (props: TerminalProps) => { <div ref={container} data-component="terminal" + data-prevent-autofocus classList={{ ...(local.classList ?? {}), "size-full px-6 py-3 font-mono": true, |
