diff --git a/bun.lock b/bun.lock index f29e42d1ee..2584dd220f 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -951,21 +951,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "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/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1cf290bf8d..c72aeea3e7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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", diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index a439fd81fc..0966ba5986 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -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" @@ -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 | "@" | "/" } @@ -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() @@ -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 () => { @@ -68,7 +115,7 @@ export function Autocomplete(props: { (item): AutocompleteOption => ({ display: item, onSelect: () => { - const part: PromptInfo["parts"][number] = { + insertPart(item, { type: "file", mime: "text/plain", filename: item, @@ -76,18 +123,12 @@ export function Autocomplete(props: { 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) }) }, }), @@ -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: "", + }, }) }, }), @@ -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) }, }) } @@ -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, @@ -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) } @@ -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 === "@") { @@ -278,7 +322,7 @@ export function Autocomplete(props: { } if (e.name === "/") { - if (props.input().cursorPosition === 0) show("/") + if (props.input().visualCursor.offset === 0) show("/") } } }, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index 04375f4585..5b90e58771 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -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 | Omit)[] + parts: ( + | Omit + | Omit + | (Omit & { + source?: { + text: { + start: number + end: number + value: string + } + } + }) + )[] } export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 72bcb9e9e4..76c39cd39a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,7 +1,15 @@ -import { InputRenderable, TextAttributes, BoxRenderable } from "@opentui/core" -import { createEffect, createMemo, Match, Switch, type JSX } from "solid-js" +import { + TextAttributes, + BoxRenderable, + TextareaRenderable, + MouseEvent, + KeyEvent, + SyntaxStyle, + PasteEvent, +} from "@opentui/core" +import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js" import { useLocal } from "@tui/context/local" -import { Theme } from "@tui/context/theme" +import { Theme, syntaxTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" @@ -11,7 +19,6 @@ import { createStore, produce } from "solid-js/store" import { useKeybind } from "@tui/context/keybind" import { usePromptHistory, type PromptInfo } from "./history" import { type AutocompleteRef, Autocomplete } from "./autocomplete" -import { iife } from "@/util/iife" import { useCommandDialog } from "../dialog-command" import { useRenderer } from "@opentui/solid" import { Editor } from "@tui/util/editor" @@ -34,7 +41,7 @@ export type PromptRef = { } export function Prompt(props: PromptProps) { - let input: InputRenderable + let input: TextareaRenderable let anchor: BoxRenderable let autocomplete: AutocompleteRef @@ -48,6 +55,11 @@ export function Prompt(props: PromptProps) { const command = useCommandDialog() const renderer = useRenderer() + const fileStyleId = syntaxTheme.getStyleId("extmark.file")! + const agentStyleId = syntaxTheme.getStyleId("extmark.agent")! + const pasteStyleId = syntaxTheme.getStyleId("extmark.paste")! + let promptPartTypeId: number + command.register(() => { return [ { @@ -57,19 +69,20 @@ export function Prompt(props: PromptProps) { value: "prompt.editor", onSelect: async (dialog) => { dialog.clear() - const value = input.value - input.value = "" + const value = input.plainText + input.clear() setStore("prompt", { input: "", parts: [], }) const content = await Editor.open({ value, renderer }) if (content) { + input.setText(content, { history: false }) setStore("prompt", { input: content, parts: [], }) - input.cursorPosition = content.length + input.cursorOffset = Bun.stringWidth(content) } }, }, @@ -79,10 +92,12 @@ export function Prompt(props: PromptProps) { disabled: true, category: "Prompt", onSelect: (dialog) => { + input.extmarks.clear() setStore("prompt", { input: "", parts: [], }) + setStore("extmarkToPartIndex", new Map()) dialog.clear() }, }, @@ -117,18 +132,102 @@ export function Prompt(props: PromptProps) { const [store, setStore] = createStore<{ prompt: PromptInfo mode: "normal" | "shell" + extmarkToPartIndex: Map }>({ prompt: { input: "", parts: [], }, mode: "normal", + extmarkToPartIndex: new Map(), }) createEffect(() => { input.focus() }) + onMount(() => { + promptPartTypeId = input.extmarks.registerType("prompt-part") + }) + + function restoreExtmarksFromParts(parts: PromptInfo["parts"]) { + input.extmarks.clear() + setStore("extmarkToPartIndex", new Map()) + + parts.forEach((part, partIndex) => { + let start = 0 + let end = 0 + let virtualText = "" + let styleId: number | undefined + + if (part.type === "file" && part.source?.text) { + start = part.source.text.start + end = part.source.text.end + virtualText = part.source.text.value + styleId = fileStyleId + } else if (part.type === "agent" && part.source) { + start = part.source.start + end = part.source.end + virtualText = part.source.value + styleId = agentStyleId + } else if (part.type === "text" && part.source?.text) { + start = part.source.text.start + end = part.source.text.end + virtualText = part.source.text.value + styleId = pasteStyleId + } + + if (virtualText) { + const extmarkId = input.extmarks.create({ + start, + end, + virtual: true, + styleId, + typeId: promptPartTypeId, + }) + setStore("extmarkToPartIndex", (map: Map) => { + const newMap = new Map(map) + newMap.set(extmarkId, partIndex) + return newMap + }) + } + }) + } + + function syncExtmarksWithPromptParts() { + const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) + setStore( + produce((draft) => { + const newMap = new Map() + const newParts: typeof draft.prompt.parts = [] + + for (const extmark of allExtmarks) { + const partIndex = draft.extmarkToPartIndex.get(extmark.id) + if (partIndex !== undefined) { + const part = draft.prompt.parts[partIndex] + if (part) { + if (part.type === "agent" && part.source) { + part.source.start = extmark.start + part.source.end = extmark.end + } else if (part.type === "file" && part.source?.text) { + part.source.text.start = extmark.start + part.source.text.end = extmark.end + } else if (part.type === "text" && part.source?.text) { + part.source.text.start = extmark.start + part.source.text.end = extmark.end + } + newMap.set(extmark.id, newParts.length) + newParts.push(part) + } + } + } + + draft.extmarkToPartIndex = newMap + draft.prompt.parts = newParts + }), + ) + } + props.ref?.({ get focused() { return input.focused @@ -140,14 +239,19 @@ export function Prompt(props: PromptProps) { input.blur() }, set(prompt) { + input.setText(prompt.input, { history: false }) setStore("prompt", prompt) - input.cursorPosition = prompt.input.length + restoreExtmarksFromParts(prompt.parts) + input.gotoBufferEnd() }, reset() { + input.clear() + input.extmarks.clear() setStore("prompt", { input: "", parts: [], }) + setStore("extmarkToPartIndex", new Map()) }, }) @@ -162,7 +266,7 @@ export function Prompt(props: PromptProps) { return sessionID })() const messageID = Identifier.ascending("message") - const input = store.prompt.input + const inputText = store.prompt.input if (store.mode === "shell") { sdk.client.session.shell({ path: { @@ -170,12 +274,12 @@ export function Prompt(props: PromptProps) { }, body: { agent: local.agent.current().name, - command: input, + command: inputText, }, }) setStore("mode", "normal") - } else if (input.startsWith("/")) { - const [command, ...args] = input.split(" ") + } else if (inputText.startsWith("/")) { + const [command, ...args] = inputText.split(" ") sdk.client.session.command({ path: { id: sessionID, @@ -202,7 +306,7 @@ export function Prompt(props: PromptProps) { { id: Identifier.ascending("part"), type: "text", - text: input, + text: inputText, }, ...store.prompt.parts.map((x) => ({ id: Identifier.ascending("part"), @@ -213,10 +317,12 @@ export function Prompt(props: PromptProps) { }) } history.append(store.prompt) + input.extmarks.clear() setStore("prompt", { input: "", parts: [], }) + setStore("extmarkToPartIndex", new Map()) props.onSubmit?.() // temporary hack to make sure the message is sent @@ -227,6 +333,7 @@ export function Prompt(props: PromptProps) { sessionID, }) }, 50) + input.clear() } const exit = useExit() @@ -239,77 +346,79 @@ export function Prompt(props: PromptProps) { input={() => input} setPrompt={(cb) => { setStore("prompt", produce(cb)) - input.cursorPosition = store.prompt.input.length + }} + setExtmark={(partIndex, extmarkId) => { + setStore("extmarkToPartIndex", (map: Map) => { + const newMap = new Map(map) + newMap.set(extmarkId, partIndex) + return newMap + }) }} value={store.prompt.input} + fileStyleId={fileStyleId} + agentStyleId={agentStyleId} + promptPartTypeId={() => promptPartTypeId} /> (anchor = r)}> - + {store.mode === "normal" ? ">" : "!"} - - { - let diff = value.length - store.prompt.input.length - setStore( - produce((draft) => { - draft.prompt.input = value - for (let i = 0; i < draft.prompt.parts.length; i++) { - const part = draft.prompt.parts[i] - if (!part.source) continue - const source = part.type === "agent" ? part.source : part.source.text - if (source.start >= input.cursorPosition) { - source.start += diff - source.end += diff - } - const sliced = draft.prompt.input.slice(source.start, source.end) - if (sliced != source.value && diff < 0) { - diff -= source.value.length - draft.prompt.input = - draft.prompt.input.slice(0, source.start) + draft.prompt.input.slice(source.end) - draft.prompt.parts.splice(i, 1) - input.cursorPosition = Math.max(0, source.start - 1) - i-- - } - } - }), - ) + +