Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
import { guard } from "../../util/input-filter"

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -836,6 +837,7 @@ export function Prompt(props: PromptProps) {
e.preventDefault()
return
}
if (guard(e)) return
// Handle clipboard paste (Ctrl+V) - check for images first on Windows
// This is needed because Windows terminal doesn't properly send image data
// through bracketed paste, so we need to intercept the keypress and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Locale } from "@/util/locale"
import { Global } from "@/global"
import { useDialog } from "../../ui/dialog"
import { useTuiConfig } from "../../context/tui-config"
import { guard } from "../../util/input-filter"

type PermissionStage = "permission" | "always" | "reject"

Expand Down Expand Up @@ -523,6 +524,9 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
focusedTextColor={theme.text}
cursorColor={theme.primary}
keyBindings={textareaKeybindings()}
onKeyDown={(evt) => {
guard(evt)
}}
/>
<box flexDirection="row" gap={2} flexShrink={0}>
<text fg={theme.text}>
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../component/border"
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useDialog } from "../../ui/dialog"
import { guard } from "../../util/input-filter"

export function QuestionPrompt(props: { request: QuestionRequest }) {
const sdk = useSDK()
Expand Down Expand Up @@ -385,6 +386,9 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
val.gotoLineEnd()
})
}}
onKeyDown={(evt) => {
guard(evt)
}}
initialValue={input()}
placeholder="Type your own answer"
minHeight={1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { onMount, Show, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { guard } from "../util/input-filter"

export type DialogExportOptionsProps = {
defaultFilename: string
Expand Down Expand Up @@ -98,6 +99,9 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
openWithoutSaving: store.openWithoutSaving,
})
}}
onKeyDown={(evt) => {
guard(evt)
}}
height={3}
keyBindings={[{ name: "return", action: "submit" }]}
ref={(val: TextareaRenderable) => (textarea = val)}
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { onMount, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { guard } from "../util/input-filter"

export type DialogPromptProps = {
title: string
Expand Down Expand Up @@ -49,6 +50,9 @@ export function DialogPrompt(props: DialogPromptProps) {
onSubmit={() => {
props.onConfirm?.(textarea.plainText)
}}
onKeyDown={(evt) => {
guard(evt)
}}
height={3}
keyBindings={[{ name: "return", action: "submit" }]}
ref={(val: TextareaRenderable) => (textarea = val)}
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as fuzzysort from "fuzzysort"
import { isDeepEqual } from "remeda"
import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { useKeybind } from "@tui/context/keybind"
import { guard } from "@tui/util/input-filter"
import { Keybind } from "@/util/keybind"
import { Locale } from "@/util/locale"

Expand Down Expand Up @@ -242,6 +243,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
</box>
<box paddingTop={1}>
<input
onKeyDown={(evt) => {
guard(evt)
}}
onInput={(e) => {
batch(() => {
setStore("filter", e)
Expand Down
25 changes: 25 additions & 0 deletions packages/opencode/src/cli/cmd/tui/util/input-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { KeyEvent, ParsedKey } from "@opentui/core"

const LOCK = new Set(["capslock", "numlock", "scrolllock"])

export function privateuse(name: string) {
const chars = [...name]
if (chars.length !== 1) return false
const code = chars[0].codePointAt(0)
if (code === undefined) return false
if (code >= 0xe000 && code <= 0xf8ff) return true
if (code >= 0xf0000 && code <= 0xffffd) return true
return code >= 0x100000 && code <= 0x10fffd
}

export function drop(key: Pick<ParsedKey, "name">) {
if (!key.name) return false
if (LOCK.has(key.name.toLowerCase())) return true
return privateuse(key.name)
}

export function guard(key: Pick<KeyEvent, "name" | "preventDefault">) {
if (!drop(key)) return false
key.preventDefault()
return true
}
29 changes: 29 additions & 0 deletions packages/opencode/test/cli/tui/input-filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test"
import { drop, privateuse } from "../../../src/cli/cmd/tui/util/input-filter"

describe("input filter", () => {
test("drops private use key events", () => {
expect(privateuse("\uE00E")).toBe(true)
expect(drop({ name: "\uE00E" })).toBe(true)
expect(privateuse("\u{F0000}")).toBe(true)
expect(drop({ name: "\u{F0000}" })).toBe(true)
})

test("keeps ascii and chinese text", () => {
expect(drop({ name: "a" })).toBe(false)
expect(drop({ name: "你" })).toBe(false)
})

test("keeps control and navigation keys", () => {
expect(drop({ name: "return" })).toBe(false)
expect(drop({ name: "backspace" })).toBe(false)
expect(drop({ name: "left" })).toBe(false)
expect(drop({ name: "space" })).toBe(false)
})

test("drops lock keys", () => {
expect(drop({ name: "capslock" })).toBe(true)
expect(drop({ name: "numlock" })).toBe(true)
expect(drop({ name: "scrolllock" })).toBe(true)
})
})
Loading