Skip to content
Merged
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
20 changes: 10 additions & 10 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.1.30",
"@opentui/solid": "0.1.30",
"@opentui/core": "0.0.0-20251027-327d7e76",
"@opentui/solid": "0.0.0-20251027-327d7e76",
"@parcel/watcher": "2.5.1",
"@solid-primitives/event-bus": "1.1.2",
"@standard-schema/spec": "1.0.0",
Expand Down Expand Up @@ -951,21 +951,21 @@

"@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],

"@opentui/core": ["@opentui/core@0.1.30", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.30", "@opentui/core-darwin-x64": "0.1.30", "@opentui/core-linux-arm64": "0.1.30", "@opentui/core-linux-x64": "0.1.30", "@opentui/core-win32-arm64": "0.1.30", "@opentui/core-win32-x64": "0.1.30", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": ">=0.26.0" } }, "sha512-DoPF3E//UaISDfp7jYhdU4KbOe7BVm9KqCV+TPMVo2lch8UfvtN2nCnHqtg54DCzxYuTbge9NDrapdt3jrT2oA=="],
"@opentui/core": ["@opentui/core@0.0.0-20251027-327d7e76", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251027-327d7e76", "@opentui/core-darwin-x64": "0.0.0-20251027-327d7e76", "@opentui/core-linux-arm64": "0.0.0-20251027-327d7e76", "@opentui/core-linux-x64": "0.0.0-20251027-327d7e76", "@opentui/core-win32-arm64": "0.0.0-20251027-327d7e76", "@opentui/core-win32-x64": "0.0.0-20251027-327d7e76", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": ">=0.26.0" } }, "sha512-9EF4TkLR4szqNmWDGYZrzti48aQ3WOaXbTKOxcAEIBNienTlvr7baNyUjwNCHsbMxsQrAIYIY7gKXjebsChxkg=="],

"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.30", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oFFBsVsrByzA/9LpJZ7uV2QWH44Hq/96eWO8PlPhMdlnvpqz5e0PYU3fQJUuDGptIvb9GZzIt5Og2gYar5bUIw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251027-327d7e76", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iOY4266FFXc1c2NqYBg5ED1YkfT5z7yVCrlLsqd9EIpWv72NIN3b9HLbY77jzsjDYqtFfOx2x96FyS8As+La3w=="],

"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.30", "", { "os": "darwin", "cpu": "x64" }, "sha512-f8MNPqwfG9qUttxcrQ3VsM+rrWvO9uHGncTJbKYEKDVaQzBuKpkEZqMSA2JV3gspjL22DXSG4Q5LE6NzvmITgQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251027-327d7e76", "", { "os": "darwin", "cpu": "x64" }, "sha512-fM441iHIG9TRRBv9y/bNeG4ZymhKi4FTtdvaPDRJ9rqljTGL/NwiP57kMiaO8Gv2y9dqzUmxDyu0+QHqexl6BQ=="],

"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.30", "", { "os": "linux", "cpu": "arm64" }, "sha512-6qBVjUU9XJrgePvFtCGmEIU+iRfyyydvbts+i7nKjJgmOFVZLnllP6XqA3s+lAjEQrzp+VM7beDV5MIOMjHHZg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251027-327d7e76", "", { "os": "linux", "cpu": "arm64" }, "sha512-fDnx9X6+UcPj5p3LnJV2RAKwEw82pabZwc6eIPsUNIWZj861/OnDOmxwDEMLJw4G+j5IBnk5oeHoNu6YeG0V1Q=="],

"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.30", "", { "os": "linux", "cpu": "x64" }, "sha512-K51WGZp7VT5aB8nbzZBt94YavMnI1yFe4h4n/e4TxZ4Ph3BACNmNHizEDfn05efcmXmMdgGOjazc5sNA2LrOOQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251027-327d7e76", "", { "os": "linux", "cpu": "x64" }, "sha512-oUNT2QuL+b3e7g22KsUGWsi1lktiorLg1xlgEB+HWpn7dtK9xX+6gc1dmXp2qtgJvXhlOWSCCBftSmVvLFn4eg=="],

"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.30", "", { "os": "win32", "cpu": "arm64" }, "sha512-NAkCu0VSHh5oj9wldjImqli7g4kvkA/3HP9PyAWB6zBg+QcXCl7Yi1WPEFwAU7XM1zEUBwhrj0sHcuMomq4Y8A=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251027-327d7e76", "", { "os": "win32", "cpu": "arm64" }, "sha512-xKFfTRO/JYTnQD+Erd9tOaEPvJPwtOVDQNQ5VSRPXdyB2YAruiZ57nnryJJ6WQjIXPy+ND4X77mylxOUFTk5Wg=="],

"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.30", "", { "os": "win32", "cpu": "x64" }, "sha512-eNIQaLm+Muluma1xXa2SxHGidjknH+iVmPPbQsV7xwJlp3a318BjTr/5/lxKBz1EGzhTaiwBqwcgTSg+Q31L5w=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251027-327d7e76", "", { "os": "win32", "cpu": "x64" }, "sha512-Ymctt059k8ZVXnLCcqZC+h1f81W0KgKPGzS3Ve/+Br/v7fwsE0mWCNuinbxrFgIHMs+6947u88rp/tyhwMQBJQ=="],

"@opentui/solid": ["@opentui/solid@0.1.30", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.30", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-7JAHQH8OLgJCjH2IBmdO3ggd7WHavEdlywhVzjoVvQYonTYcJjcJ2F6MJ1Qmqqm6y2+IYv3KIYMQm6HLxX5TxQ=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251027-327d7e76", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251027-327d7e76", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-/eTp1WbjRQs2tVqXPgAVKlhMmzJP60SZ+ZJ8S4cqdQ12g++WrFWCMb8OcueLs/rgKHr07WORo7Aw9/a9I1vV/w=="],

"@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],

Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.1.30",
"@opentui/solid": "0.1.30",
"@opentui/core": "0.0.0-20251027-327d7e76",
"@opentui/solid": "0.0.0-20251027-327d7e76",
"@parcel/watcher": "2.5.1",
"@solid-primitives/event-bus": "1.1.2",
"@standard-schema/spec": "1.0.0",
Expand Down
108 changes: 76 additions & 32 deletions packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ParsedKey, BoxRenderable, InputRenderable } from "@opentui/core"
import type { BoxRenderable, TextareaRenderable, KeyEvent } from "@opentui/core"
import fuzzysort from "fuzzysort"
import { firstBy } from "remeda"
import { createMemo, createResource, createEffect, onMount, For, Show } from "solid-js"
Expand All @@ -12,7 +12,7 @@ import type { PromptInfo } from "./history"

export type AutocompleteRef = {
onInput: (value: string) => void
onKeyDown: (e: ParsedKey) => void
onKeyDown: (e: KeyEvent) => void
visible: false | "@" | "/"
}

Expand All @@ -27,9 +27,13 @@ export function Autocomplete(props: {
value: string
sessionID?: string
setPrompt: (input: (prompt: PromptInfo) => void) => void
setExtmark: (partIndex: number, extmarkId: number) => void
anchor: () => BoxRenderable
input: () => InputRenderable
input: () => TextareaRenderable
ref: (ref: AutocompleteRef) => void
fileStyleId: number
agentStyleId: number
promptPartTypeId: () => number
}) {
const sdk = useSDK()
const sync = useSync()
Expand All @@ -46,6 +50,49 @@ export function Autocomplete(props: {
return props.value.substring(store.index + 1).split(" ")[0]
})

function insertPart(text: string, part: PromptInfo["parts"][number]) {
const append = "@" + text + " "
const input = props.input()
const currentCursorOffset = input.visualCursor.offset

input.cursorOffset = store.index
const startCursor = input.logicalCursor
input.cursorOffset = currentCursorOffset
const endCursor = input.logicalCursor

input.deleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)
input.insertText(append)

const virtualText = "@" + text
const extmarkStart = store.index
const extmarkEnd = extmarkStart + virtualText.length

const styleId = part.type === "file" ? props.fileStyleId : part.type === "agent" ? props.agentStyleId : undefined

const extmarkId = input.extmarks.create({
start: extmarkStart,
end: extmarkEnd,
virtual: true,
styleId,
typeId: props.promptPartTypeId(),
})

props.setPrompt((draft) => {
if (part.type === "file" && part.source?.text) {
part.source.text.start = extmarkStart
part.source.text.end = extmarkEnd
part.source.text.value = virtualText
} else if (part.type === "agent" && part.source) {
part.source.start = extmarkStart
part.source.end = extmarkEnd
part.source.value = virtualText
}
const partIndex = draft.parts.length
draft.parts.push(part)
props.setExtmark(partIndex, extmarkId)
})
}

const [files] = createResource(
() => [filter()],
async () => {
Expand All @@ -68,26 +115,20 @@ export function Autocomplete(props: {
(item): AutocompleteOption => ({
display: item,
onSelect: () => {
const part: PromptInfo["parts"][number] = {
insertPart(item, {
type: "file",
mime: "text/plain",
filename: item,
url: `file://${process.cwd()}/${item}`,
source: {
type: "file",
text: {
start: store.index,
end: store.index + item.length + 1,
value: "@" + item,
start: 0,
end: 0,
value: "",
},
path: item,
},
}
props.setPrompt((draft) => {
const append = "@" + item + " "
if (store.index === 0) draft.input = append
if (store.index > 0) draft.input = draft.input.slice(0, store.index) + append
draft.parts.push(part)
})
},
}),
Expand All @@ -111,18 +152,14 @@ export function Autocomplete(props: {
(agent): AutocompleteOption => ({
display: "@" + agent.name,
onSelect: () => {
props.setPrompt((draft) => {
const append = "@" + agent.name + " "
draft.input = append
draft.parts.push({
type: "agent",
source: {
start: store.index,
end: store.index + agent.name.length + 1,
value: "@" + agent.name,
},
name: agent.name,
})
insertPart(agent.name, {
type: "agent",
name: agent.name,
source: {
start: 0,
end: 0,
value: "",
},
})
},
}),
Expand All @@ -138,8 +175,11 @@ export function Autocomplete(props: {
display: "/" + command.name,
description: command.description,
onSelect: () => {
props.input().value = "/" + command.name + " "
props.input().cursorPosition = props.input().value.length
const newText = "/" + command.name + " "
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
props.input().insertText(newText)
props.input().cursorOffset = Bun.stringWidth(newText)
},
})
}
Expand Down Expand Up @@ -234,13 +274,13 @@ export function Autocomplete(props: {
const selected = options()[store.selected]
if (!selected) return
selected.onSelect?.()
setTimeout(() => hide(), 0)
hide()
}

function show(mode: "@" | "/") {
setStore({
visible: mode,
index: props.input().cursorPosition,
index: props.input().visualCursor.offset,
position: {
x: props.anchor().x,
y: props.anchor().y,
Expand All @@ -250,7 +290,10 @@ export function Autocomplete(props: {
}

function hide() {
if (store.visible === "/" && !props.value.endsWith(" ")) props.input().value = ""
if (store.visible === "/" && !props.value.endsWith(" ")) {
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
}
setStore("visible", false)
}

Expand All @@ -262,12 +305,13 @@ export function Autocomplete(props: {
onInput(value: string) {
if (store.visible && value.length <= store.index) hide()
},
onKeyDown(e: ParsedKey) {
onKeyDown(e: KeyEvent) {
if (store.visible) {
if (e.name === "up") move(-1)
if (e.name === "down") move(1)
if (e.name === "escape") hide()
if (e.name === "return") select()
if (["up", "down", "return", "escape"].includes(e.name)) e.preventDefault()
}
if (!store.visible) {
if (e.name === "@") {
Expand All @@ -278,7 +322,7 @@ export function Autocomplete(props: {
}

if (e.name === "/") {
if (props.input().cursorPosition === 0) show("/")
if (props.input().visualCursor.offset === 0) show("/")
}
}
},
Expand Down
16 changes: 14 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ import { createStore, produce } from "solid-js/store"
import { clone } from "remeda"
import { createSimpleContext } from "../../context/helper"
import { appendFile } from "fs/promises"
import type { AgentPart, FilePart } from "@opencode-ai/sdk"
import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk"

export type PromptInfo = {
input: string
parts: (Omit<FilePart, "id" | "messageID" | "sessionID"> | Omit<AgentPart, "id" | "messageID" | "sessionID">)[]
parts: (
| Omit<FilePart, "id" | "messageID" | "sessionID">
| Omit<AgentPart, "id" | "messageID" | "sessionID">
| (Omit<TextPart, "id" | "messageID" | "sessionID"> & {
source?: {
text: {
start: number
end: number
value: string
}
}
})
)[]
}

export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
Expand Down
Loading