mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Apply PR #40427: some experimental perf improvements
This commit is contained in:
@@ -17,7 +17,7 @@ import { installSseTransport } from "../utils/sse-transport"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const initialPageSize = 20
|
||||
const historyPageSize = 200
|
||||
const historyPageSize = 50
|
||||
const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => {
|
||||
const id = `msg_${String(index + 1001).padStart(4, "0")}_history_root_user`
|
||||
return [
|
||||
|
||||
@@ -52,10 +52,9 @@ import { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||
import Layout from "@/pages/layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { useCheckServerHealth } from "./utils/server-health"
|
||||
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||
import { legacySessionServer, sessionHref } from "./utils/session-route"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
|
||||
import { TargetSessionRouteContent } from "@/pages/session"
|
||||
import { TargetSessionRoute } from "@/pages/session-lazy"
|
||||
import { Home } from "@/pages/home"
|
||||
|
||||
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||
@@ -76,30 +75,6 @@ const DirectoryDraftRedirect = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
function TargetServerRoute(props: ParentProps) {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const global = useGlobal()
|
||||
const conn = createMemo(() => {
|
||||
const key = requireServerKey(params.serverKey)
|
||||
return global.servers.list().find((item) => ServerConnection.key(item) === key)
|
||||
})
|
||||
|
||||
return (
|
||||
// Owns the server-identity remount. Session changes must not remount this subtree.
|
||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||
<ServerSDKProvider server={conn}>
|
||||
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetSessionRoute = () => (
|
||||
<TargetServerRoute>
|
||||
<TargetSessionRouteContent />
|
||||
</TargetServerRoute>
|
||||
)
|
||||
|
||||
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
||||
// server via ServerKey, then provide the server-scoped shell for that server.
|
||||
function SelectedServerProviders(props: ParentProps) {
|
||||
|
||||
@@ -16,7 +16,7 @@ type MessageApi = ServerApi["message"]
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const initialMessagePageSize = 20
|
||||
const historyMessagePageSize = 200
|
||||
const historyMessagePageSize = 50
|
||||
const sessionInfoLimit = 2_048
|
||||
const emptyIDs: ReadonlySet<string> = new Set()
|
||||
|
||||
@@ -45,6 +45,12 @@ function projectMessageSource(message: Message): SessionMessageInfo[] {
|
||||
]
|
||||
}
|
||||
|
||||
function yieldToMain() {
|
||||
const scheduler = (globalThis as { scheduler?: { yield: () => Promise<void> } }).scheduler
|
||||
if (scheduler) return scheduler.yield()
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) {
|
||||
const boundary = source.find(
|
||||
(message) =>
|
||||
@@ -538,6 +544,7 @@ export function createServerSession(
|
||||
if (!response.data.length) break
|
||||
}
|
||||
const response = pages.at(-1)!
|
||||
await yieldToMain()
|
||||
const source = pages.flatMap((page) => page.data).toReversed()
|
||||
const normalized = normalizeSessionMessages(sessionID, source)
|
||||
return {
|
||||
|
||||
@@ -28,3 +28,4 @@ export {
|
||||
} from "./wsl/types"
|
||||
export { ServerConnection } from "./context/server"
|
||||
export { createDraftStore, type DraftStore } from "./utils/draft-store"
|
||||
export { preloadSessionRoute } from "./pages/session-lazy"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { createHomeController } from "./home/home-controller"
|
||||
import { createHomeProjectsController } from "./home/home-projects-controller"
|
||||
import { HomeUtilityNav } from "./home/home-projects-view"
|
||||
@@ -7,8 +8,23 @@ import { createHomeScrollController } from "./home/home-scroll-controller"
|
||||
import { createHomeSessionSearchController } from "./home/home-session-search-controller"
|
||||
import { createHomeSessionsController } from "./home/home-sessions-controller"
|
||||
import { HomeSessions } from "./home/home-sessions"
|
||||
import { preloadSessionRoute } from "./session-lazy"
|
||||
|
||||
export function Home() {
|
||||
onMount(() => {
|
||||
let idle: number | undefined
|
||||
const timer = setTimeout(() => {
|
||||
if ("requestIdleCallback" in window) {
|
||||
idle = requestIdleCallback(() => void preloadSessionRoute(), { timeout: 3_000 })
|
||||
return
|
||||
}
|
||||
void preloadSessionRoute()
|
||||
}, 1_500)
|
||||
onCleanup(() => {
|
||||
clearTimeout(timer)
|
||||
if (idle !== undefined) cancelIdleCallback(idle)
|
||||
})
|
||||
})
|
||||
const home = createHomeController()
|
||||
const projects = createHomeProjectsController(home)
|
||||
const sessions = createHomeSessionsController(home)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { lazy } from "solid-js"
|
||||
|
||||
export const TargetSessionRoute = lazy(() => import("./target-session-route"))
|
||||
export const preloadSessionRoute = TargetSessionRoute.preload
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||
import { ServerSyncProvider } from "@/context/server-sync"
|
||||
import { requireServerKey } from "@/utils/session-route"
|
||||
import { TargetSessionRouteContent } from "./session"
|
||||
|
||||
export default function TargetSessionRoute() {
|
||||
const params = useParams<{ serverKey: string }>()
|
||||
const global = useGlobal()
|
||||
const connection = createMemo(() => {
|
||||
const key = requireServerKey(params.serverKey)
|
||||
return global.servers.list().find((item) => ServerConnection.key(item) === key)
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||
<ServerSDKProvider server={connection}>
|
||||
<ServerSyncProvider server={connection}>
|
||||
<TargetSessionRouteContent />
|
||||
</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type Locale,
|
||||
type Platform,
|
||||
PlatformProvider,
|
||||
preloadSessionRoute,
|
||||
createDraftStore,
|
||||
ServerConnection,
|
||||
useCommand,
|
||||
@@ -442,7 +443,9 @@ render(() => {
|
||||
const api = window.api as typeof window.api & {
|
||||
getWindowID?: () => Promise<string>
|
||||
}
|
||||
return { id: await api.getWindowID?.() }
|
||||
const id = await api.getWindowID?.()
|
||||
if (/^\/server\/[^/]+\/session\/[^/]+/.test(getLastActiveUrl(id ?? "browser"))) await preloadSessionRoute()
|
||||
return { id }
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -158,6 +158,12 @@ describe("markdown stream", () => {
|
||||
expect(final.blocks[2]).toEqual({ raw: "- final item", src: "- final item", mode: "full" })
|
||||
})
|
||||
|
||||
test("splits completed markdown into bounded top-level blocks", () => {
|
||||
const result = project(undefined, "# Plan\n\nFirst paragraph.\n\nSecond paragraph.", false)
|
||||
|
||||
expect(result.blocks.map((block) => block.raw)).toEqual(["# Plan", "First paragraph.", "Second paragraph."])
|
||||
})
|
||||
|
||||
test("catches up paced text before finalizing", () => {
|
||||
const live = project(undefined, "# Plan\n\nFinished paragraph.\n\n- final", true)
|
||||
const final = project(live, `${live.text} item`, false)
|
||||
|
||||
@@ -51,7 +51,7 @@ function heal(text: string) {
|
||||
}
|
||||
|
||||
export function stream(text: string, live: boolean): Block[] {
|
||||
if (!live) return completedProjection(text).blocks
|
||||
if (!live) return completedBlocks(text)
|
||||
if (refs(text)) return [{ raw: text, src: heal(text), mode: "live" }] satisfies Block[]
|
||||
const tokens = marked.lexer(text)
|
||||
const tail = tokens.findLastIndex((token) => token.type !== "space")
|
||||
@@ -85,6 +85,17 @@ export function stream(text: string, live: boolean): Block[] {
|
||||
return [...result, { raw, src: openCode(code.raw), mode: "code", language: language(code.lang) }]
|
||||
}
|
||||
|
||||
function completedBlocks(text: string) {
|
||||
if (refs(text)) return completedProjection(text).blocks
|
||||
const tokens = marked.lexer(text)
|
||||
return tokens.flatMap((token): Block[] => {
|
||||
if (token.type === "space") return []
|
||||
if (token.type !== "code") return [{ raw: token.raw, src: token.raw, mode: "full" }]
|
||||
const code = token as Tokens.Code
|
||||
return [{ raw: code.raw, src: code.text, mode: "code", language: language(code.lang), complete: true }]
|
||||
})
|
||||
}
|
||||
|
||||
export function project(previous: Projection | undefined, text: string, live: boolean): Projection {
|
||||
if (!live) {
|
||||
const current =
|
||||
@@ -93,7 +104,7 @@ export function project(previous: Projection | undefined, text: string, live: bo
|
||||
: previous && text.startsWith(previous.text)
|
||||
? project(previous, text, true)
|
||||
: undefined
|
||||
if (!current) return completedProjection(text)
|
||||
if (!current) return { text, blocks: completedBlocks(text) }
|
||||
return {
|
||||
text,
|
||||
blocks: current.blocks.map((block) => {
|
||||
|
||||
@@ -491,6 +491,8 @@ export function Markdown(
|
||||
)
|
||||
|
||||
let copyCleanup: (() => void) | undefined
|
||||
let renderFrame: number | undefined
|
||||
let renderGeneration = 0
|
||||
|
||||
createEffect(() => {
|
||||
const container = root()
|
||||
@@ -499,6 +501,9 @@ export function Markdown(
|
||||
const content = local.text ? pendingBlocks(result, projected, local.cacheKey, owner) : []
|
||||
if (!container) return
|
||||
if (isServer) return
|
||||
const generation = ++renderGeneration
|
||||
if (renderFrame !== undefined) cancelAnimationFrame(renderFrame)
|
||||
renderFrame = undefined
|
||||
if (content.length === 0) {
|
||||
disposeCopyButtons(container)
|
||||
container.innerHTML = ""
|
||||
@@ -515,24 +520,40 @@ export function Markdown(
|
||||
})
|
||||
activeCodeKeys.clear()
|
||||
nextCodeKeys.forEach((key) => activeCodeKeys.add(key))
|
||||
content.forEach((block, index) => updateBlock(container, index, block, labels))
|
||||
while (container.children.length > content.length) {
|
||||
const child = container.lastElementChild
|
||||
if (!child) break
|
||||
disposeCopyButtons(child)
|
||||
child.remove()
|
||||
let index = 0
|
||||
const update = () => {
|
||||
renderFrame = undefined
|
||||
if (generation !== renderGeneration) return
|
||||
const deadline = performance.now() + 8
|
||||
while (index < content.length && performance.now() < deadline) {
|
||||
updateBlock(container, index, content[index]!, labels)
|
||||
index += 1
|
||||
}
|
||||
if (index < content.length) {
|
||||
renderFrame = requestAnimationFrame(update)
|
||||
return
|
||||
}
|
||||
while (container.children.length > content.length) {
|
||||
const child = container.lastElementChild
|
||||
if (!child) break
|
||||
disposeCopyButtons(child)
|
||||
child.remove()
|
||||
}
|
||||
container
|
||||
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
|
||||
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
|
||||
if (!copyCleanup)
|
||||
copyCleanup = setupCodeCopy(container, () => ({
|
||||
copy: i18n.t("ui.message.copy"),
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
}))
|
||||
}
|
||||
container
|
||||
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
|
||||
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
|
||||
if (!copyCleanup)
|
||||
copyCleanup = setupCodeCopy(container, () => ({
|
||||
copy: i18n.t("ui.message.copy"),
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
}))
|
||||
update()
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
renderGeneration += 1
|
||||
if (renderFrame !== undefined) cancelAnimationFrame(renderFrame)
|
||||
if (copyCleanup) copyCleanup()
|
||||
disposeMarkdownProjection(owner)
|
||||
activeCodeKeys.forEach(disposeCode)
|
||||
|
||||
@@ -24,7 +24,7 @@ function createPool(lineDiffType: "none" | "word-alt") {
|
||||
{
|
||||
theme: "OpenCode",
|
||||
lineDiffType,
|
||||
preferredHighlighter: "shiki-wasm",
|
||||
preferredHighlighter: "shiki-js",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -53,9 +53,11 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
const view = props.controller.view
|
||||
let editor: HTMLDivElement | undefined
|
||||
let localInput = false
|
||||
const updateCursor = () => {
|
||||
const updateCursor = (event: KeyboardEvent | PointerEvent) => {
|
||||
if (!editor || !window.getSelection()?.isCollapsed) return
|
||||
props.controller.onCursor(promptInputV2Cursor(editor))
|
||||
if (event instanceof KeyboardEvent && !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key))
|
||||
return
|
||||
props.controller.onCursor(parsePromptInputV2Editor(editor).cursor)
|
||||
}
|
||||
const mode = createMemo(() => state.mode)
|
||||
const buttons = createMemo(() => ({
|
||||
@@ -164,8 +166,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none empty:before:content-['\200B'] [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
|
||||
classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }}
|
||||
onInput={(event) => {
|
||||
const cursor = promptInputV2Cursor(event.currentTarget)
|
||||
const prompt = parsePromptInputV2Editor(event.currentTarget)
|
||||
const { prompt, cursor } = parsePromptInputV2Editor(event.currentTarget)
|
||||
const images = props.controller.parts().filter((part) => part.type === "image")
|
||||
localInput = true
|
||||
props.controller.onInput(prompt.map((part) => part.content).join(""), [...prompt, ...images], cursor)
|
||||
@@ -302,8 +303,13 @@ function renderPromptInputV2Editor(editor: HTMLDivElement, prompt: PromptInputV2
|
||||
|
||||
function parsePromptInputV2Editor(editor: HTMLDivElement) {
|
||||
const parts: Exclude<PromptInputV2Prompt[number], PromptInputV2Attachment>[] = []
|
||||
const selection = window.getSelection()
|
||||
const anchorNode = selection && editor.contains(selection.anchorNode) ? selection.anchorNode : undefined
|
||||
const anchorOffset = anchorNode ? selection!.anchorOffset : 0
|
||||
let buffer = ""
|
||||
let position = 0
|
||||
let cursor: number | undefined
|
||||
const offset = () => position + buffer.length
|
||||
|
||||
const flush = () => {
|
||||
if (!buffer) return
|
||||
@@ -338,43 +344,42 @@ function parsePromptInputV2Editor(editor: HTMLDivElement) {
|
||||
}
|
||||
const visit = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
if (node === anchorNode) cursor = offset() + Math.min(anchorOffset, node.textContent?.length ?? 0)
|
||||
buffer += node.textContent ?? ""
|
||||
return
|
||||
}
|
||||
if (!(node instanceof HTMLElement)) return
|
||||
if (node.dataset.mention) {
|
||||
if (node === anchorNode) cursor = offset() + (anchorOffset > 0 ? (node.textContent?.length ?? 0) : 0)
|
||||
mention(node)
|
||||
return
|
||||
}
|
||||
if (node.tagName === "BR") {
|
||||
if (node === anchorNode) cursor = offset() + (anchorOffset > 0 ? 1 : 0)
|
||||
buffer += "\n"
|
||||
return
|
||||
}
|
||||
Array.from(node.childNodes).forEach(visit)
|
||||
Array.from(node.childNodes).forEach((child, index) => {
|
||||
if (node === anchorNode && anchorOffset === index) cursor = offset()
|
||||
visit(child)
|
||||
})
|
||||
if (node === anchorNode && anchorOffset >= node.childNodes.length) cursor = offset()
|
||||
}
|
||||
|
||||
Array.from(editor.childNodes).forEach((node, index, nodes) => {
|
||||
if (editor === anchorNode && anchorOffset === index) cursor = offset()
|
||||
visit(node)
|
||||
if (node instanceof HTMLElement && ["DIV", "P"].includes(node.tagName) && index < nodes.length - 1) buffer += "\n"
|
||||
})
|
||||
if (editor === anchorNode && anchorOffset >= editor.childNodes.length) cursor = offset()
|
||||
flush()
|
||||
if (
|
||||
parts.every((part) => part.type === "text") &&
|
||||
parts.every((part) => part.content.replace(/[\n\u200B]/g, "") === "")
|
||||
) {
|
||||
return [{ type: "text" as const, content: "", start: 0, end: 0 }]
|
||||
}
|
||||
if (parts.length > 0) return parts
|
||||
return [{ type: "text" as const, content: "", start: 0, end: 0 }]
|
||||
}
|
||||
|
||||
function promptInputV2Cursor(editor: HTMLDivElement) {
|
||||
const selection = window.getSelection()
|
||||
if (!selection?.rangeCount || !editor.contains(selection.anchorNode)) return editor.textContent?.length ?? 0
|
||||
const range = selection.getRangeAt(0).cloneRange()
|
||||
range.selectNodeContents(editor)
|
||||
range.setEnd(selection.anchorNode!, selection.anchorOffset)
|
||||
return range.toString().length
|
||||
const result =
|
||||
parts.length === 0 ||
|
||||
(parts.every((part) => part.type === "text") &&
|
||||
parts.every((part) => part.content.replace(/[\n\u200B]/g, "") === ""))
|
||||
? [{ type: "text" as const, content: "", start: 0, end: 0 }]
|
||||
: parts
|
||||
return { prompt: result, cursor: cursor ?? offset() }
|
||||
}
|
||||
|
||||
export function PromptInputV2Attachments(props: {
|
||||
|
||||
Reference in New Issue
Block a user