Compare commits

...

1 Commits

Author SHA1 Message Date
Adam
871a0e11b9 fix(app): more startup perf 2026-03-25 11:28:59 -05:00
18 changed files with 274 additions and 93 deletions

View File

@@ -133,7 +133,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
return (
<MetaProvider>
<Font />
<Font preloadMono={false} />
<ThemeProvider
onThemeApplied={(_, mode) => {
void window.api?.setTitlebar?.({ mode })

View File

@@ -17,6 +17,7 @@ import {
type JSXElement,
type ParentProps,
} from "solid-js"
import { createStore, type SetStoreFunction } from "solid-js/store"
import { Dynamic } from "solid-js/web"
import type { FileNode } from "@opencode-ai/sdk/v2"
@@ -201,6 +202,7 @@ export default function FileTree(props: {
modified?: readonly string[]
kinds?: ReadonlyMap<string, Kind>
draggable?: boolean
synthetic?: boolean
onFileClick?: (file: FileNode) => void
_filter?: Filter
@@ -208,10 +210,15 @@ export default function FileTree(props: {
_deeps?: Map<string, number>
_kinds?: ReadonlyMap<string, Kind>
_chain?: readonly string[]
_open?: Record<string, boolean>
_setOpen?: SetStoreFunction<Record<string, boolean>>
}) {
const file = useFile()
const level = props.level ?? 0
const draggable = () => props.draggable ?? true
const local = createStore<Record<string, boolean>>({})
const open = props._open ?? local[0]
const setOpen = props._setOpen ?? local[1]
const key = (p: string) =>
file
@@ -258,6 +265,7 @@ export default function FileTree(props: {
const deeps = createMemo(() => {
if (props._deeps) return props._deeps
if (props.synthetic) return new Map<string, number>()
const out = new Map<string, number>()
@@ -304,6 +312,7 @@ export default function FileTree(props: {
})
createEffect(() => {
if (props.synthetic) return
const current = filter()
const dirs = dirsToExpand({
level,
@@ -317,6 +326,7 @@ export default function FileTree(props: {
on(
() => props.path,
(path) => {
if (props.synthetic) return
const dir = untrack(() => file.tree.state(path))
if (!shouldListRoot({ level, dir })) return
void file.tree.list(path)
@@ -388,7 +398,8 @@ export default function FileTree(props: {
<div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
<For each={nodes()}>
{(node) => {
const expanded = () => file.tree.state(node.path)?.expanded ?? false
const expanded = () =>
props.synthetic ? (open[node.path] ?? true) : (file.tree.state(node.path)?.expanded ?? false)
const deep = () => deeps().get(node.path) ?? -1
const kind = () => visibleKind(node, kinds(), marks())
const active = () => !!kind() && !node.ignored
@@ -402,7 +413,13 @@ export default function FileTree(props: {
data-scope="filetree"
forceMount={false}
open={expanded()}
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
onOpenChange={(open) => {
if (props.synthetic) {
setOpen(node.path, open)
return
}
open ? file.tree.expand(node.path) : file.tree.collapse(node.path)
}}
>
<Collapsible.Trigger>
<FileTreeNode
@@ -435,6 +452,7 @@ export default function FileTree(props: {
<FileTree
path={node.path}
level={level + 1}
synthetic={props.synthetic}
allowed={props.allowed}
modified={props.modified}
kinds={props.kinds}
@@ -446,6 +464,8 @@ export default function FileTree(props: {
_deeps={deeps()}
_kinds={kinds()}
_chain={chain}
_open={open}
_setOpen={setOpen}
/>
</Show>
</Collapsible.Content>

View File

@@ -27,7 +27,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
import { useCommand } from "@/context/command"
import { Persist, persisted } from "@/utils/persist"
@@ -1494,7 +1493,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
size="normal"
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
style={control()}
onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)}
onClick={() => {
void import("@/components/dialog-select-model-unpaid").then((x) =>
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />),
)
}}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon

View File

@@ -243,12 +243,6 @@ export async function bootstrapDirectory(input: {
]
const slow = [
() =>
retry(() =>
input.sdk.provider.list().then((x) => {
input.setStore("provider", normalizeProviderList(x.data!))
}),
),
() => Promise.resolve(input.loadSessions(input.directory)),
() => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
() => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),

View File

@@ -56,6 +56,22 @@ function cleanupSessionCaches(
)
}
function keep(next: Session, prev?: Session) {
const diffs = prev?.summary?.diffs
const files = prev?.summary?.files
if (!diffs?.length) return next
if (!next.summary || next.summary.diffs?.length) return next
if (next.summary.files <= 0) return next
if (next.summary.files !== files) return next
return {
...next,
summary: {
...next.summary,
diffs,
},
}
}
export function cleanupDroppedSessionCaches(
store: Store<State>,
setStore: SetStoreFunction<State>,
@@ -105,7 +121,7 @@ export function applyDirectoryEvent(input: {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (result.found) {
input.setStore("session", result.index, reconcile(info))
input.setStore("session", result.index, reconcile(keep(info, input.store.session[result.index])))
break
}
const next = input.store.session.slice()
@@ -134,7 +150,7 @@ export function applyDirectoryEvent(input: {
break
}
if (result.found) {
input.setStore("session", result.index, reconcile(info))
input.setStore("session", result.index, reconcile(keep(info, input.store.session[result.index])))
break
}
const next = input.store.session.slice()

View File

@@ -1,5 +1,5 @@
import { createStore, reconcile } from "solid-js/store"
import { createEffect, createMemo } from "solid-js"
import { createEffect, createMemo, onCleanup } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { persisted } from "@/utils/persist"
@@ -120,7 +120,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
if (typeof document === "undefined") return
const id = store.appearance?.font ?? defaultSettings.appearance.font
if (id !== defaultSettings.appearance.font) {
void loadFont().then((x) => x.ensureMonoFont(id))
const run = () => {
void loadFont().then((x) => x.ensureMonoFont(id))
}
if (typeof requestIdleCallback === "function") {
const idle = requestIdleCallback(run, { timeout: 2000 })
onCleanup(() => cancelIdleCallback(idle))
} else {
const timeout = window.setTimeout(run, 2000)
onCleanup(() => window.clearTimeout(timeout))
}
}
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
})

View File

@@ -180,8 +180,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return globalSync.child(directory)
}
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
const initialMessagePageSize = 80
const historyMessagePageSize = 200
const initialMessagePageSize = 40
const historyMessagePageSize = 80
const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>()
const inflightTodo = new Map<string, Promise<void>>()
@@ -460,13 +460,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
}
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
const hit = Binary.search(store.session, sessionID, (s) => s.id)
const session = hit.found ? store.session[hit.index] : undefined
const hasSession = hit.found
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
if (cached && hasSession && !opts?.force) return
const needs = !!session?.summary?.files && !session.summary?.diffs
if (cached && hasSession && !opts?.force && !needs) return
const limit = meta.limit[key] ?? initialMessagePageSize
const sessionReq =
hasSession && !opts?.force
hasSession && !opts?.force && !needs
? Promise.resolve()
: retry(() => client.session.get({ sessionID })).then((session) => {
if (!tracked(directory, sessionID)) return

View File

@@ -21,7 +21,7 @@ export function useProviders() {
const dir = createMemo(() => decode64(params.dir) ?? "")
const providers = () => {
if (dir()) {
const [projectStore] = globalSync.child(dir())
const [projectStore] = globalSync.peek(dir(), { bootstrap: false })
if (projectStore.provider.all.length > 0) return projectStore.provider
}
return globalSync.data.provider

View File

@@ -158,6 +158,13 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
preserveScroll(() => setTurnStart(nextStart))
}
const reveal = () => {
const start = turnStart()
if (start <= 0) return false
backfillTurns()
return true
}
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
const loadAndReveal = async () => {
const id = input.sessionID()
@@ -303,6 +310,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
return {
turnStart,
setTurnStart,
reveal,
renderedUserMessages,
loadAndReveal,
onScrollerScroll,
@@ -877,6 +885,7 @@ export default function Page() {
}
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
const wantsDiff = createMemo(() => (isDesktop() ? desktopReviewOpen() && activeTab() === "review" : mobileChanges()))
const fileTreeTab = () => layout.fileTree.tab()
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
@@ -1074,6 +1083,7 @@ export default function Page() {
}
const focusReviewDiff = (path: string) => {
void tabs().open("review")
openReviewPanel()
view().review.openPath(path)
setTree({ activeDiff: path, pendingDiff: path })
@@ -1124,10 +1134,7 @@ export default function Page() {
const id = params.id
if (!id) return
const wants = isDesktop()
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
: store.mobileTab === "changes"
if (!wants) return
if (!wantsDiff()) return
if (sync.data.session_diff[id] !== undefined) return
if (sync.status === "loading") return
@@ -1136,13 +1143,7 @@ export default function Page() {
createEffect(
on(
() =>
[
sessionKey(),
isDesktop()
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
: store.mobileTab === "changes",
] as const,
() => [sessionKey(), wantsDiff()] as const,
([key, wants]) => {
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
@@ -1167,19 +1168,6 @@ export default function Page() {
),
)
let treeDir: string | undefined
createEffect(() => {
const dir = sdk.directory
if (!isDesktop()) return
if (!layout.fileTree.opened()) return
if (sync.status === "loading") return
fileTreeTab()
const refresh = treeDir !== dir
treeDir = dir
void (refresh ? file.tree.refresh("") : file.tree.list(""))
})
createEffect(
on(
() => sdk.directory,
@@ -1296,9 +1284,9 @@ export default function Page() {
const el = scroller
if (!el) return
if (el.scrollHeight > el.clientHeight + 1) return
if (historyWindow.turnStart() <= 0 && !historyMore()) return
if (historyWindow.turnStart() <= 0) return
void historyWindow.loadAndReveal()
historyWindow.reveal()
})
}
@@ -1309,14 +1297,13 @@ export default function Page() {
params.id,
messagesReady(),
historyWindow.turnStart(),
historyMore(),
historyLoading(),
autoScroll.userScrolled(),
visibleUserMessages().length,
] as const,
([id, ready, start, more, loading, scrolled]) => {
([id, ready, start, loading, scrolled]) => {
if (!id || !ready || loading || scrolled) return
if (start <= 0 && !more) return
if (start <= 0) return
fill()
},
{ defer: true },

View File

@@ -49,7 +49,6 @@ export const createSessionTabs = (input: TabsInput) => {
const first = openedTabs()[0]
if (first) return first
if (contextOpen()) return "context"
if (review() && hasReview()) return "review"
return "empty"
})
const activeFileTab = createMemo(() => {

View File

@@ -13,7 +13,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import FileTree from "@/components/file-tree"
import { SessionContextUsage } from "@/components/session-context-usage"
import { DialogSelectFile } from "@/components/dialog-select-file"
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
import { useCommand } from "@/context/command"
import { useFile, type SelectedLineRange } from "@/context/file"
@@ -56,14 +55,15 @@ export function SessionSidePanel(props: {
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
const changes = createMemo(() => {
const id = params.id
if (!id) return []
const full = sync.data.session_diff[id]
if (full !== undefined) return full
return info()?.summary?.diffs ?? []
})
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
const hasReview = createMemo(() => reviewCount() > 0)
const diffsReady = createMemo(() => {
const id = params.id
if (!id) return true
if (!hasReview()) return true
return sync.data.session_diff[id] !== undefined
})
const reviewEmptyKey = createMemo(() => {
if (sync.project && !sync.project.vcs) return "session.review.noVcs"
@@ -71,7 +71,7 @@ export function SessionSidePanel(props: {
return "session.review.noChanges"
})
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const diffFiles = createMemo(() => changes().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
if (!a) return b
@@ -82,7 +82,7 @@ export function SessionSidePanel(props: {
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
const out = new Map<string, "add" | "del" | "mix">()
for (const diff of diffs()) {
for (const diff of changes()) {
const file = normalize(diff.file)
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
@@ -293,9 +293,11 @@ export function SessionSidePanel(props: {
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() =>
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
}
onClick={() => {
void import("@/components/dialog-select-file").then((x) =>
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />),
)
}}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
@@ -386,26 +388,17 @@ export function SessionSidePanel(props: {
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={hasReview()}>
<Show
when={diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</div>
}
>
<FileTree
path=""
class="pt-3"
allowed={diffFiles()}
kinds={kinds()}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/>
</Show>
<Match when={hasReview() && diffFiles().length > 0}>
<FileTree
synthetic
path=""
class="pt-3"
allowed={diffFiles()}
kinds={kinds()}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/>
</Match>
<Match when={true}>
{empty(

View File

@@ -11,10 +11,6 @@ import { usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal"
import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { DialogFork } from "@/components/dialog-fork"
import { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/util/array"
import { createSessionTabs } from "@/pages/session/helpers"
@@ -257,7 +253,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
description: language.t("palette.search.placeholder"),
keybind: "mod+k,mod+p",
slash: "open",
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
onSelect: () => {
void import("@/components/dialog-select-file").then((x) =>
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />),
)
},
}),
fileCommand({
id: "tab.close",
@@ -351,7 +351,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
description: language.t("command.model.choose.description"),
keybind: "mod+'",
slash: "model",
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
onSelect: () => {
void import("@/components/dialog-select-model").then((x) =>
dialog.show(() => <x.DialogSelectModel model={local.model} />),
)
},
}),
mcpCommand({
id: "mcp.toggle",
@@ -359,7 +363,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
description: language.t("command.mcp.toggle.description"),
keybind: "mod+;",
slash: "mcp",
onSelect: () => dialog.show(() => <DialogSelectMcp />),
onSelect: () => {
void import("@/components/dialog-select-mcp").then((x) => dialog.show(() => <x.DialogSelectMcp />))
},
}),
agentCommand({
id: "agent.cycle",
@@ -487,7 +493,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
description: language.t("command.session.fork.description"),
slash: "fork",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: () => dialog.show(() => <DialogFork />),
onSelect: () => {
void import("@/components/dialog-fork").then((x) => dialog.show(() => <x.DialogFork />))
},
}),
...share,
]

View File

@@ -123,6 +123,15 @@ export const SessionRoutes = lazy(() =>
const sessionID = c.req.valid("param").sessionID
log.info("SEARCH", { url: c.req.url })
const session = await Session.get(sessionID)
if (session.summary?.files) {
const diffs = await SessionSummary.list(sessionID)
if (diffs.length > 0) {
session.summary = {
...session.summary,
diffs,
}
}
}
return c.json(session)
},
)

View File

@@ -51,6 +51,58 @@ globalThis.AI_SDK_LOG_WARNINGS = false
const csp = (hash = "") =>
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
const api = (path: string) =>
path === "/agent" ||
path === "/command" ||
path === "/formatter" ||
path === "/log" ||
path === "/lsp" ||
path === "/path" ||
path === "/skill" ||
path === "/vcs" ||
path.startsWith("/auth/") ||
path.startsWith("/config") ||
path.startsWith("/experimental") ||
path.startsWith("/global") ||
path.startsWith("/mcp") ||
path.startsWith("/permission") ||
path.startsWith("/project") ||
path.startsWith("/provider") ||
path.startsWith("/question") ||
path.startsWith("/session")
const json = (value: string | null) => {
const type = value?.split(";")[0]?.trim()
return type === "application/json" || type?.endsWith("+json")
}
const gzip = (value?: string) => {
if (!value) return false
let star = false
for (const item of value.split(",")) {
const [name, ...params] = item.trim().toLowerCase().split(";")
const q = params.find((part) => part.trim().startsWith("q="))
const score = q ? Number(q.trim().slice(2)) : 1
const ok = !Number.isNaN(score) && score > 0
if (name === "gzip") return ok
if (name === "*") star = ok
}
return star
}
const vary = (headers: Headers, value: string) => {
const current = headers.get("Vary")
if (!current) {
headers.set("Vary", value)
return
}
if (current.split(",").some((item) => item.trim().toLowerCase() === value.toLowerCase())) return
headers.set("Vary", `${current}, ${value}`)
}
export namespace Server {
const log = Log.create({ service: "server" })
@@ -104,6 +156,26 @@ export namespace Server {
timer.stop()
}
})
.use(async (c, next) => {
await next()
if (!api(c.req.path)) return
if (c.req.method === "HEAD") return
if (c.res.headers.has("Content-Encoding")) return
if (c.res.headers.has("Transfer-Encoding")) return
if (!json(c.res.headers.get("Content-Type"))) return
if (!gzip(c.req.header("Accept-Encoding"))) return
const size = Number(c.res.headers.get("Content-Length") ?? "")
if (Number.isFinite(size) && size > 0 && size < 1024) return
if (!c.res.body) return
c.res = new Response(c.res.body.pipeThrough(new CompressionStream("gzip")), c.res)
c.res.headers.delete("Content-Length")
c.res.headers.set("Content-Encoding", "gzip")
vary(c.res.headers, "Accept-Encoding")
})
.use(
cors({
origin(input) {

View File

@@ -12,6 +12,14 @@ import { Bus } from "@/bus"
import { NotFoundError } from "@/storage/db"
export namespace SessionSummary {
function shape(diffs: Snapshot.FileDiff[]) {
return diffs.map((item) => ({
...item,
before: "",
after: "",
}))
}
function unquoteGitPath(input: string) {
if (!input.startsWith('"')) return input
if (!input.endsWith('"')) return input
@@ -141,6 +149,8 @@ export namespace SessionSummary {
},
)
export const list = fn(SessionID.zod, async (sessionID) => shape(await diff({ sessionID })))
export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
let from: string | undefined
let to: string | undefined

View File

@@ -5,6 +5,31 @@ import { type Config } from "./gen/client/types.gen.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export { type Config as OpencodeClientConfig, OpencodeClient }
const keys = [
["x-opencode-directory", "directory"],
["x-opencode-workspace", "workspace"],
] as const
function move(req: Request) {
if (req.method !== "GET" && req.method !== "HEAD") return req
let url: URL | undefined
for (const [header, key] of keys) {
const value = req.headers.get(header)
if (!value) continue
url ??= new URL(req.url)
if (!url.searchParams.has(key)) url.searchParams.set(key, value)
}
if (!url) return req
const next = new Request(url, req)
for (const [header] of keys) {
next.headers.delete(header)
}
return next
}
export function createOpencodeClient(config?: Config & { directory?: string }) {
if (!config?.fetch) {
const customFetch: any = (req: any) => {
@@ -26,5 +51,8 @@ export function createOpencodeClient(config?: Config & { directory?: string }) {
}
const client = createClient(config)
if (typeof window === "object" && typeof document === "object") {
client.interceptors.request.use(move)
}
return new OpencodeClient({ client })
}

View File

@@ -5,6 +5,31 @@ import { type Config } from "./gen/client/types.gen.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export { type Config as OpencodeClientConfig, OpencodeClient }
const keys = [
["x-opencode-directory", "directory"],
["x-opencode-workspace", "workspace"],
] as const
function move(req: Request) {
if (req.method !== "GET" && req.method !== "HEAD") return req
let url: URL | undefined
for (const [header, key] of keys) {
const value = req.headers.get(header)
if (!value) continue
url ??= new URL(req.url)
if (!url.searchParams.has(key)) url.searchParams.set(key, value)
}
if (!url) return req
const next = new Request(url, req)
for (const [header] of keys) {
next.headers.delete(header)
}
return next
}
export function createOpencodeClient(config?: Config & { directory?: string; experimental_workspaceID?: string }) {
if (!config?.fetch) {
const customFetch: any = (req: any) => {
@@ -35,5 +60,8 @@ export function createOpencodeClient(config?: Config & { directory?: string; exp
}
const client = createClient(config)
if (typeof window === "object" && typeof document === "object") {
client.interceptors.request.use(move)
}
return new OpencodeClient({ client })
}

View File

@@ -5,7 +5,7 @@ import ibmPlexMonoBold from "../assets/fonts/ibm-plex-mono-bold.woff2"
import ibmPlexMonoMedium from "../assets/fonts/ibm-plex-mono-medium.woff2"
import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2"
export const Font = () => {
export const Font = (props: { preloadMono?: boolean }) => {
return (
<>
<Style>{`
@@ -56,7 +56,9 @@ export const Font = () => {
`}</Style>
<Show when={typeof location === "undefined" || location.protocol !== "file:"}>
<Link rel="preload" href={inter} as="font" type="font/woff2" crossorigin="anonymous" />
<Link rel="preload" href={ibmPlexMonoRegular} as="font" type="font/woff2" crossorigin="anonymous" />
<Show when={props.preloadMono !== false}>
<Link rel="preload" href={ibmPlexMonoRegular} as="font" type="font/woff2" crossorigin="anonymous" />
</Show>
</Show>
</>
)