refactor(server): canonicalize service API (#31049)

This commit is contained in:
Dax
2026-06-07 03:27:28 +00:00
committed by GitHub
parent 53ff1b57c9
commit fe0c4f8c74
388 changed files with 7103 additions and 4092 deletions
+48
View File
@@ -0,0 +1,48 @@
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
export function promptOffsetWidth(value: string) {
let width = 0
for (const part of graphemes.segment(value)) {
// Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.
width += part.segment === "\n" ? 1 : Bun.stringWidth(part.segment)
}
return width
}
function displayOffsetIndex(value: string, offset: number) {
if (offset <= 0) return 0
let width = 0
for (const part of graphemes.segment(value)) {
const next = width + promptOffsetWidth(part.segment)
if (next > offset) return part.index
width = next
}
return value.length
}
export function displaySlice(value: string, start = 0, end = promptOffsetWidth(value)) {
return value.slice(displayOffsetIndex(value, start), displayOffsetIndex(value, end))
}
export function displayCharAt(value: string, offset: number) {
let width = 0
for (const part of graphemes.segment(value)) {
const next = width + promptOffsetWidth(part.segment)
if (offset === width || offset < next) return part.segment
width = next
}
}
export function mentionTriggerIndex(value: string, offset = promptOffsetWidth(value)) {
const text = displaySlice(value, 0, offset)
const index = text.lastIndexOf("@")
if (index === -1) return
const before = index === 0 ? undefined : text[index - 1]
const query = text.slice(index)
if ((before === undefined || /\s/.test(before)) && !/\s/.test(query)) {
return promptOffsetWidth(text.slice(0, index))
}
}
+80
View File
@@ -0,0 +1,80 @@
import path from "path"
import { onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "../context/helper"
import { useTuiEnvironment } from "../runtime"
import { appendText, readText, writeText } from "../util/persistence"
type FrecencyEntry = { path: string; frequency: number; lastOpen: number }
export const MAX_FRECENCY_ENTRIES = 1000
export function parseFrecency(text: string) {
const latest = text
.split("\n")
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line) as FrecencyEntry
} catch {
return undefined
}
})
.filter((line): line is FrecencyEntry => line !== undefined)
.reduce<Record<string, FrecencyEntry>>((result, entry) => {
result[entry.path] = entry
return result
}, {})
return Object.values(latest)
.sort((a, b) => b.lastOpen - a.lastOpen)
.slice(0, MAX_FRECENCY_ENTRIES)
}
function calculateFrecency(entry?: { frequency: number; lastOpen: number }) {
if (!entry) return 0
return entry.frequency / (1 + (Date.now() - entry.lastOpen) / 86400000)
}
export const { use: useFrecency, provider: FrecencyProvider } = createSimpleContext({
name: "Frecency",
init: () => {
const environment = useTuiEnvironment()
const frecencyPath = path.join(environment.paths.state, "frecency.jsonl")
onMount(async () => {
const lines = parseFrecency(await readText(frecencyPath).catch(() => ""))
setStore(
"data",
Object.fromEntries(
lines.map((entry) => [entry.path, { frequency: entry.frequency, lastOpen: entry.lastOpen }]),
),
)
if (lines.length > 0)
writeText(frecencyPath, lines.map((entry) => JSON.stringify(entry)).join("\n") + "\n").catch(() => {})
})
const [store, setStore] = createStore({ data: {} as Record<string, { frequency: number; lastOpen: number }> })
function updateFrecency(filePath: string) {
const absolutePath = path.resolve(environment.cwd, filePath)
const newEntry = { frequency: (store.data[absolutePath]?.frequency || 0) + 1, lastOpen: Date.now() }
setStore("data", absolutePath, newEntry)
appendText(frecencyPath, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {})
if (Object.keys(store.data).length <= MAX_FRECENCY_ENTRIES) return
const sorted = Object.entries(store.data)
.sort(([, a], [, b]) => b.lastOpen - a.lastOpen)
.slice(0, MAX_FRECENCY_ENTRIES)
setStore("data", Object.fromEntries(sorted))
writeText(
frecencyPath,
sorted.map(([entryPath, entry]) => JSON.stringify({ path: entryPath, ...entry })).join("\n") + "\n",
).catch(() => {})
}
return {
getFrecency: (filePath: string) => calculateFrecency(store.data[path.resolve(environment.cwd, filePath)]),
updateFrecency,
data: () => store.data,
}
},
})
+111
View File
@@ -0,0 +1,111 @@
import path from "path"
import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store"
import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "../context/helper"
import { useTuiEnvironment } from "../runtime"
import { appendText, readText, writeText } from "../util/persistence"
export type PromptInfo = {
input: string
mode?: "normal" | "shell"
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 MAX_HISTORY_ENTRIES = 50
export function parsePromptHistory(text: string) {
return text
.split("\n")
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line) as PromptInfo
} catch {
return undefined
}
})
.filter((line): line is PromptInfo => line !== undefined)
.slice(-MAX_HISTORY_ENTRIES)
}
export function isDuplicateEntry(previous: PromptInfo | undefined, next: PromptInfo): boolean {
if (!previous) return false
return JSON.stringify(previous) === JSON.stringify(next)
}
export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
name: "PromptHistory",
init: () => {
const environment = useTuiEnvironment()
const historyPath = path.join(environment.paths.state, "prompt-history.jsonl")
onMount(async () => {
const lines = parsePromptHistory(await readText(historyPath).catch(() => ""))
setStore("history", lines)
// Rewrite valid retained entries to self-heal corruption and enforce the limit.
if (lines.length > 0)
writeText(historyPath, lines.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
})
const [store, setStore] = createStore({
index: 0,
history: [] as PromptInfo[],
})
return {
move(direction: 1 | -1, input: string) {
if (!store.history.length) return undefined
const current = store.history.at(store.index)
if (!current) return undefined
if (current.input !== input && input.length) return
setStore(
produce((draft) => {
const next = store.index + direction
if (Math.abs(next) > store.history.length) return
if (next > 0) return
draft.index = next
}),
)
if (store.index === 0) return { input: "", parts: [] }
return store.history.at(store.index)
},
append(item: PromptInfo) {
const entry = structuredClone(unwrap(item))
if (isDuplicateEntry(store.history.at(-1), entry)) {
setStore("index", 0)
return
}
let trimmed = false
setStore(
produce((draft) => {
draft.history.push(entry)
if (draft.history.length > MAX_HISTORY_ENTRIES) {
draft.history = draft.history.slice(-MAX_HISTORY_ENTRIES)
trimmed = true
}
draft.index = 0
}),
)
if (trimmed) {
writeText(historyPath, store.history.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
return
}
appendText(historyPath, JSON.stringify(entry) + "\n").catch(() => {})
},
}
},
})
+32
View File
@@ -0,0 +1,32 @@
import { displaySlice } from "./display"
export function stripPromptPartIDs<Part extends { id: string; messageID: string; sessionID: string }>(part: Part) {
const { id: _id, messageID: _messageID, sessionID: _sessionID, ...rest } = part
return rest
}
export function expandPastedTextPlaceholders(
text: string,
parts: readonly unknown[],
) {
return parts.reduce<string>((result, part) => {
if (!isPastedTextPart(part)) return result
return result.replace(part.source.text.value, part.text)
}, text)
}
function isPastedTextPart(part: unknown): part is { type: "text"; text: string; source: { text: { value: string } } } {
if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text") return false
if (!("text" in part) || typeof part.text !== "string" || !("source" in part)) return false
const source = part.source
if (!source || typeof source !== "object" || !("text" in source)) return false
const text = source.text
return Boolean(text && typeof text === "object" && "value" in text && typeof text.value === "string")
}
export function expandTrackedPastedText(text: string, ranges: { start: number; end: number; text: string }[]) {
return ranges
.slice()
.sort((a, b) => b.start - a.start)
.reduce((result, part) => displaySlice(result, 0, part.start) + part.text + displaySlice(result, part.end), text)
}
+89
View File
@@ -0,0 +1,89 @@
import path from "path"
import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../context/helper"
import { useTuiEnvironment } from "../runtime"
import { appendText, readText, writeText } from "../util/persistence"
import type { PromptInfo } from "./history"
export type StashEntry = {
input: string
parts: PromptInfo["parts"]
timestamp: number
}
export const MAX_STASH_ENTRIES = 50
export function parsePromptStash(text: string) {
return text
.split("\n")
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line) as StashEntry
} catch {
return undefined
}
})
.filter((line): line is StashEntry => line !== undefined)
.slice(-MAX_STASH_ENTRIES)
}
export const { use: usePromptStash, provider: PromptStashProvider } = createSimpleContext({
name: "PromptStash",
init: () => {
const environment = useTuiEnvironment()
const stashPath = path.join(environment.paths.state, "prompt-stash.jsonl")
onMount(async () => {
const lines = parsePromptStash(await readText(stashPath).catch(() => ""))
setStore("entries", lines)
if (lines.length > 0)
writeText(stashPath, lines.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
})
const [store, setStore] = createStore({ entries: [] as StashEntry[] })
return {
list() {
return store.entries
},
push(entry: Omit<StashEntry, "timestamp">) {
const stash = structuredClone(unwrap({ ...entry, timestamp: Date.now() }))
let trimmed = false
setStore(
produce((draft) => {
draft.entries.push(stash)
if (draft.entries.length > MAX_STASH_ENTRIES) {
draft.entries = draft.entries.slice(-MAX_STASH_ENTRIES)
trimmed = true
}
}),
)
if (trimmed) {
writeText(stashPath, store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
return
}
appendText(stashPath, JSON.stringify(stash) + "\n").catch(() => {})
},
pop() {
if (store.entries.length === 0) return undefined
const entry = store.entries[store.entries.length - 1]
setStore(produce((draft) => void draft.entries.pop()))
writeText(
stashPath,
store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "",
).catch(() => {})
return entry
},
remove(index: number) {
if (index < 0 || index >= store.entries.length) return
setStore(produce((draft) => void draft.entries.splice(index, 1)))
writeText(
stashPath,
store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "",
).catch(() => {})
},
}
},
})
+29
View File
@@ -0,0 +1,29 @@
import type { EditorTraits } from "@opentui/core"
export type PromptMode = "normal" | "shell"
export interface PromptTraitsInput {
mode: PromptMode
autocompleteVisible: boolean
}
export type PromptTraits = EditorTraits & {
owner: "opencode"
role: "prompt"
}
/** The managed textarea keymap owns `suspend`; these traits only describe capture and status. */
export function computePromptTraits(input: PromptTraitsInput): PromptTraits {
const capture =
input.mode === "normal"
? input.autocompleteVisible
? (["escape", "navigate", "submit", "tab"] as const)
: (["tab"] as const)
: undefined
return {
capture,
status: input.mode === "shell" ? "SHELL" : undefined,
owner: "opencode",
role: "prompt",
}
}