refactor(tui): extract shared frontend helpers (#37868)

This commit is contained in:
Simon Klee 2026-07-20 10:43:02 +02:00 committed by GitHub
commit 0eb71d0fc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1655 additions and 1614 deletions

View file

@ -22,38 +22,7 @@ import { Keymap } from "../../context/keymap"
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
import type { FileSystemEntry } from "@opencode-ai/client"
import { stringWidth } from "../../util/string-width"
function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
return hashIndex !== -1 ? input.substring(0, hashIndex) : input
}
function extractLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
if (hashIndex === -1) {
return { baseQuery: input }
}
const baseName = input.substring(0, hashIndex)
const linePart = input.substring(hashIndex + 1)
const lineMatch = linePart.match(/^(\d+)(?:-(\d*))?$/)
if (!lineMatch) {
return { baseQuery: baseName }
}
const startLine = Number(lineMatch[1])
const endLine = lineMatch[2] && startLine < Number(lineMatch[2]) ? Number(lineMatch[2]) : undefined
return {
lineRange: {
baseName,
startLine,
endLine,
},
baseQuery: baseName,
}
}
import { parseFileLineRange, stripFileLineRange } from "../../prompt/parse"
export type AutocompleteRef = {
onInput: (value: string) => void
@ -275,9 +244,9 @@ export function Autocomplete(props: {
const referenceMatch = createMemo(() => {
if (!store.visible || store.visible === "/") return
const { baseQuery } = extractLineRange(search())
const slash = baseQuery.indexOf("/")
const alias = slash === -1 ? baseQuery : baseQuery.slice(0, slash)
const base = parseFileLineRange(search()).base
const slash = base.indexOf("/")
const alias = slash === -1 ? base : base.slice(0, slash)
return references().find((item) => !item.hidden && item.name === alias)
})
@ -312,11 +281,11 @@ export function Autocomplete(props: {
async (input) => {
if (!input.visible || input.visible === "/") return { options: [], failed: false }
if (referenceMatch()) return { options: [], failed: false }
const { lineRange, baseQuery } = extractLineRange(input.query ?? "")
const { lineRange, base } = parseFileLineRange(input.query ?? "")
const result = await client.api.file
.find({
query: baseQuery,
query: base,
limit: 20,
location: {
directory: input.location?.directory,
@ -500,9 +469,9 @@ export function Autocomplete(props: {
}
const fuzziedNonFiles = fuzzysort
.go(removeLineRange(searchValue), nonFileOptions, {
.go(stripFileLineRange(searchValue), nonFileOptions, {
keys: [
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
(obj) => stripFileLineRange((obj.value ?? obj.display).trimEnd()),
// Match description for slash commands only; for "@" it surfaced unrelated items.
...(store.visible === "/" ? ["description" as const] : []),
(obj) => obj.aliases?.join(" ") ?? "",

View file

@ -26,6 +26,8 @@ import { editorSelectionKey, useEditorContext, type EditorSelection } from "../.
import { normalizePromptContent, openEditor } from "../../editor"
import { useExit } from "../../context/exit"
import { promptOffsetWidth } from "../../prompt/display"
import { expandPromptInputPastedText, realignPromptInputMentions } from "../../prompt/mention"
import { parseSlashHead } from "../../prompt/parse"
import { stringWidth } from "../../util/string-width"
import { createStore, produce, unwrap } from "solid-js/store"
import { emptyPrompt, usePromptHistory, type PromptInfo, type PromptPartRef } from "../../prompt/history"
@ -137,15 +139,15 @@ function formatEditorContext(selection: EditorSelection) {
let stashed: { prompt: PromptInfo; cursor: number } | undefined
function argumentSlash(input: string, commands: readonly KeymapCommand[]) {
if (!input.startsWith("/")) return
const separator = input.search(/\s/)
const name = input.slice(1, separator === -1 ? undefined : separator)
const head = parseSlashHead(input, /\s/)
if (!head) return
const command = commands.find(
(command) =>
command.slash?.arguments && (command.slash.name === name || command.slash.aliases?.includes(name) === true),
command.slash?.arguments &&
(command.slash.name === head.name || command.slash.aliases?.includes(head.name) === true),
)
if (!command) return
return { command, input: separator === -1 ? "" : input.slice(separator + 1) }
return { command, input: head.arguments }
}
export function Prompt(props: PromptProps) {
@ -493,13 +495,8 @@ export function Prompt(props: PromptProps) {
run: async () => {
dialog.clear()
// replace summarized text parts with the actual text
const text = store.prompt.pasted.reduce(
(result, part) => result.replace(part.source.text, part.text),
store.prompt.text,
)
const value = text
const editorPrompt = expandPromptInputPastedText(store.prompt, store.prompt.pasted)
const value = editorPrompt.text
const content = await openEditor({
renderer,
value,
@ -513,20 +510,8 @@ export function Prompt(props: PromptProps) {
input.setText(normalized)
// Update attachment positions and drop virtual text deleted in the editor.
// this handles a case where the user edits the text in the editor
// such that the virtual text moves around or is deleted
const moveMention = <Part extends { mention?: { start: number; end: number; text: string } }>(part: Part) => {
if (!part.mention?.text) return part
const start = normalized.indexOf(part.mention.text)
if (start === -1) return
return { ...part, mention: { ...part.mention, start, end: start + part.mention.text.length } }
}
setStore("prompt", {
text: normalized,
files: store.prompt.files?.map(moveMention).filter((part) => part !== undefined),
agents: store.prompt.agents?.map(moveMention).filter((part) => part !== undefined),
...realignPromptInputMentions(normalized, editorPrompt),
pasted: [],
})
restoreExtmarksFromPrompt(store.prompt)
@ -1122,7 +1107,7 @@ export function Prompt(props: PromptProps) {
if (
session?.model?.providerID !== selectedModel.providerID ||
session.model.id !== selectedModel.modelID ||
session.model.variant !== variant
(session.model.variant ?? "default") !== (variant ?? "default")
) {
await client.api.session.switchModel({
sessionID,