summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/prompt-input
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 19:58:43 -0600
committerAdam <[email protected]>2026-02-12 19:58:57 -0600
commit7f95cc64c57b439f58833d0300a1da93b3b893df (patch)
treed9a75b27dac2f1fcf5865bb3c98fbee28f489eb5 /packages/app/src/components/prompt-input
parentb525c03d205e37ad7527e6bd1749b324395dd6b7 (diff)
downloadopencode-7f95cc64c57b439f58833d0300a1da93b3b893df.tar.gz
opencode-7f95cc64c57b439f58833d0300a1da93b3b893df.zip
fix(app): prompt input quirks
Diffstat (limited to 'packages/app/src/components/prompt-input')
-rw-r--r--packages/app/src/components/prompt-input/editor-dom.test.ts36
-rw-r--r--packages/app/src/components/prompt-input/editor-dom.ts2
-rw-r--r--packages/app/src/components/prompt-input/history.test.ts24
-rw-r--r--packages/app/src/components/prompt-input/history.ts7
4 files changed, 61 insertions, 8 deletions
diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts
index fce8b4b95..15e759f44 100644
--- a/packages/app/src/components/prompt-input/editor-dom.test.ts
+++ b/packages/app/src/components/prompt-input/editor-dom.test.ts
@@ -2,17 +2,26 @@ import { describe, expect, test } from "bun:test"
import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom"
describe("prompt-input editor dom", () => {
- test("createTextFragment preserves newlines with br and zero-width placeholders", () => {
+ test("createTextFragment preserves newlines with consecutive br nodes", () => {
const fragment = createTextFragment("foo\n\nbar")
const container = document.createElement("div")
container.appendChild(fragment)
- expect(container.childNodes.length).toBe(5)
+ expect(container.childNodes.length).toBe(4)
+ expect(container.childNodes[0]?.textContent).toBe("foo")
+ expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
+ expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR")
+ expect(container.childNodes[3]?.textContent).toBe("bar")
+ })
+
+ test("createTextFragment keeps trailing newline as terminal break", () => {
+ const fragment = createTextFragment("foo\n")
+ const container = document.createElement("div")
+ container.appendChild(fragment)
+
+ expect(container.childNodes.length).toBe(2)
expect(container.childNodes[0]?.textContent).toBe("foo")
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
- expect(container.childNodes[2]?.textContent).toBe("\u200B")
- expect((container.childNodes[3] as HTMLElement).tagName).toBe("BR")
- expect(container.childNodes[4]?.textContent).toBe("bar")
})
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
@@ -48,4 +57,21 @@ describe("prompt-input editor dom", () => {
container.remove()
})
+
+ test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
+ const container = document.createElement("div")
+ container.appendChild(document.createTextNode("a"))
+ container.appendChild(document.createElement("br"))
+ container.appendChild(document.createElement("br"))
+ container.appendChild(document.createTextNode("b"))
+ document.body.appendChild(container)
+
+ setCursorPosition(container, 2)
+ expect(getCursorPosition(container)).toBe(2)
+
+ setCursorPosition(container, 3)
+ expect(getCursorPosition(container)).toBe(3)
+
+ container.remove()
+ })
})
diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts
index 3116ceb12..4850a26ec 100644
--- a/packages/app/src/components/prompt-input/editor-dom.ts
+++ b/packages/app/src/components/prompt-input/editor-dom.ts
@@ -4,8 +4,6 @@ export function createTextFragment(content: string): DocumentFragment {
segments.forEach((segment, index) => {
if (segment) {
fragment.appendChild(document.createTextNode(segment))
- } else if (segments.length > 1) {
- fragment.appendChild(document.createTextNode("\u200B"))
}
if (index < segments.length - 1) {
fragment.appendChild(document.createElement("br"))
diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts
index 54be9cb75..a37fdad67 100644
--- a/packages/app/src/components/prompt-input/history.test.ts
+++ b/packages/app/src/components/prompt-input/history.test.ts
@@ -1,6 +1,12 @@
import { describe, expect, test } from "bun:test"
import type { Prompt } from "@/context/prompt"
-import { clonePromptParts, navigatePromptHistory, prependHistoryEntry, promptLength } from "./history"
+import {
+ canNavigateHistoryAtCursor,
+ clonePromptParts,
+ navigatePromptHistory,
+ prependHistoryEntry,
+ promptLength,
+} from "./history"
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
@@ -66,4 +72,20 @@ describe("prompt-input history", () => {
if (original[1]?.type !== "file") throw new Error("expected file")
expect(original[1].selection?.startLine).toBe(1)
})
+
+ test("canNavigateHistoryAtCursor only allows multiline boundaries", () => {
+ const value = "a\nb\nc"
+
+ expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
+ expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
+
+ expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
+ expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false)
+
+ expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
+ expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
+
+ expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(true)
+ expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(true)
+ })
})
diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts
index 63164f0ba..f26f80848 100644
--- a/packages/app/src/components/prompt-input/history.ts
+++ b/packages/app/src/components/prompt-input/history.ts
@@ -4,6 +4,13 @@ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
export const MAX_HISTORY = 100
+export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number) {
+ if (!text.includes("\n")) return true
+ const position = Math.max(0, Math.min(cursor, text.length))
+ if (direction === "up") return !text.slice(0, position).includes("\n")
+ return !text.slice(position).includes("\n")
+}
+
export function clonePromptParts(prompt: Prompt): Prompt {
return prompt.map((part) => {
if (part.type === "text") return { ...part }