fix(app): more startup perf
parent
0c0c6f3bdb
commit
871a0e11b9
|
|
@ -133,7 +133,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font preloadMono={false} />
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
onThemeApplied={(_, mode) => {
|
onThemeApplied={(_, mode) => {
|
||||||
void window.api?.setTitlebar?.({ mode })
|
void window.api?.setTitlebar?.({ mode })
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
type JSXElement,
|
type JSXElement,
|
||||||
type ParentProps,
|
type ParentProps,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
|
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
|
@ -201,6 +202,7 @@ export default function FileTree(props: {
|
||||||
modified?: readonly string[]
|
modified?: readonly string[]
|
||||||
kinds?: ReadonlyMap<string, Kind>
|
kinds?: ReadonlyMap<string, Kind>
|
||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
|
synthetic?: boolean
|
||||||
onFileClick?: (file: FileNode) => void
|
onFileClick?: (file: FileNode) => void
|
||||||
|
|
||||||
_filter?: Filter
|
_filter?: Filter
|
||||||
|
|
@ -208,10 +210,15 @@ export default function FileTree(props: {
|
||||||
_deeps?: Map<string, number>
|
_deeps?: Map<string, number>
|
||||||
_kinds?: ReadonlyMap<string, Kind>
|
_kinds?: ReadonlyMap<string, Kind>
|
||||||
_chain?: readonly string[]
|
_chain?: readonly string[]
|
||||||
|
_open?: Record<string, boolean>
|
||||||
|
_setOpen?: SetStoreFunction<Record<string, boolean>>
|
||||||
}) {
|
}) {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const level = props.level ?? 0
|
const level = props.level ?? 0
|
||||||
const draggable = () => props.draggable ?? true
|
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) =>
|
const key = (p: string) =>
|
||||||
file
|
file
|
||||||
|
|
@ -258,6 +265,7 @@ export default function FileTree(props: {
|
||||||
|
|
||||||
const deeps = createMemo(() => {
|
const deeps = createMemo(() => {
|
||||||
if (props._deeps) return props._deeps
|
if (props._deeps) return props._deeps
|
||||||
|
if (props.synthetic) return new Map<string, number>()
|
||||||
|
|
||||||
const out = new Map<string, number>()
|
const out = new Map<string, number>()
|
||||||
|
|
||||||
|
|
@ -304,6 +312,7 @@ export default function FileTree(props: {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
if (props.synthetic) return
|
||||||
const current = filter()
|
const current = filter()
|
||||||
const dirs = dirsToExpand({
|
const dirs = dirsToExpand({
|
||||||
level,
|
level,
|
||||||
|
|
@ -317,6 +326,7 @@ export default function FileTree(props: {
|
||||||
on(
|
on(
|
||||||
() => props.path,
|
() => props.path,
|
||||||
(path) => {
|
(path) => {
|
||||||
|
if (props.synthetic) return
|
||||||
const dir = untrack(() => file.tree.state(path))
|
const dir = untrack(() => file.tree.state(path))
|
||||||
if (!shouldListRoot({ level, dir })) return
|
if (!shouldListRoot({ level, dir })) return
|
||||||
void file.tree.list(path)
|
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 ?? ""}`}>
|
<div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||||
<For each={nodes()}>
|
<For each={nodes()}>
|
||||||
{(node) => {
|
{(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 deep = () => deeps().get(node.path) ?? -1
|
||||||
const kind = () => visibleKind(node, kinds(), marks())
|
const kind = () => visibleKind(node, kinds(), marks())
|
||||||
const active = () => !!kind() && !node.ignored
|
const active = () => !!kind() && !node.ignored
|
||||||
|
|
@ -402,7 +413,13 @@ export default function FileTree(props: {
|
||||||
data-scope="filetree"
|
data-scope="filetree"
|
||||||
forceMount={false}
|
forceMount={false}
|
||||||
open={expanded()}
|
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>
|
<Collapsible.Trigger>
|
||||||
<FileTreeNode
|
<FileTreeNode
|
||||||
|
|
@ -435,6 +452,7 @@ export default function FileTree(props: {
|
||||||
<FileTree
|
<FileTree
|
||||||
path={node.path}
|
path={node.path}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
|
synthetic={props.synthetic}
|
||||||
allowed={props.allowed}
|
allowed={props.allowed}
|
||||||
modified={props.modified}
|
modified={props.modified}
|
||||||
kinds={props.kinds}
|
kinds={props.kinds}
|
||||||
|
|
@ -446,6 +464,8 @@ export default function FileTree(props: {
|
||||||
_deeps={deeps()}
|
_deeps={deeps()}
|
||||||
_kinds={kinds()}
|
_kinds={kinds()}
|
||||||
_chain={chain}
|
_chain={chain}
|
||||||
|
_open={open}
|
||||||
|
_setOpen={setOpen}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
@ -1494,7 +1493,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
size="normal"
|
size="normal"
|
||||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||||
style={control()}
|
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}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
|
|
|
||||||
|
|
@ -243,12 +243,6 @@ export async function bootstrapDirectory(input: {
|
||||||
]
|
]
|
||||||
|
|
||||||
const slow = [
|
const slow = [
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.provider.list().then((x) => {
|
|
||||||
input.setStore("provider", normalizeProviderList(x.data!))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
() => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
|
() => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
|
||||||
() => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
|
() => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
export function cleanupDroppedSessionCaches(
|
||||||
store: Store<State>,
|
store: Store<State>,
|
||||||
setStore: SetStoreFunction<State>,
|
setStore: SetStoreFunction<State>,
|
||||||
|
|
@ -105,7 +121,7 @@ export function applyDirectoryEvent(input: {
|
||||||
const info = (event.properties as { info: Session }).info
|
const info = (event.properties as { info: Session }).info
|
||||||
const result = Binary.search(input.store.session, info.id, (s) => s.id)
|
const result = Binary.search(input.store.session, info.id, (s) => s.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setStore("session", result.index, reconcile(info))
|
input.setStore("session", result.index, reconcile(keep(info, input.store.session[result.index])))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const next = input.store.session.slice()
|
const next = input.store.session.slice()
|
||||||
|
|
@ -134,7 +150,7 @@ export function applyDirectoryEvent(input: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setStore("session", result.index, reconcile(info))
|
input.setStore("session", result.index, reconcile(keep(info, input.store.session[result.index])))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const next = input.store.session.slice()
|
const next = input.store.session.slice()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
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 { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { persisted } from "@/utils/persist"
|
import { persisted } from "@/utils/persist"
|
||||||
|
|
||||||
|
|
@ -120,7 +120,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
const id = store.appearance?.font ?? defaultSettings.appearance.font
|
const id = store.appearance?.font ?? defaultSettings.appearance.font
|
||||||
if (id !== 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))
|
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -180,8 +180,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||||
return globalSync.child(directory)
|
return globalSync.child(directory)
|
||||||
}
|
}
|
||||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||||
const initialMessagePageSize = 80
|
const initialMessagePageSize = 40
|
||||||
const historyMessagePageSize = 200
|
const historyMessagePageSize = 80
|
||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
const inflightTodo = 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
|
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 limit = meta.limit[key] ?? initialMessagePageSize
|
||||||
const sessionReq =
|
const sessionReq =
|
||||||
hasSession && !opts?.force
|
hasSession && !opts?.force && !needs
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: retry(() => client.session.get({ sessionID })).then((session) => {
|
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||||
if (!tracked(directory, sessionID)) return
|
if (!tracked(directory, sessionID)) return
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export function useProviders() {
|
||||||
const dir = createMemo(() => decode64(params.dir) ?? "")
|
const dir = createMemo(() => decode64(params.dir) ?? "")
|
||||||
const providers = () => {
|
const providers = () => {
|
||||||
if (dir()) {
|
if (dir()) {
|
||||||
const [projectStore] = globalSync.child(dir())
|
const [projectStore] = globalSync.peek(dir(), { bootstrap: false })
|
||||||
if (projectStore.provider.all.length > 0) return projectStore.provider
|
if (projectStore.provider.all.length > 0) return projectStore.provider
|
||||||
}
|
}
|
||||||
return globalSync.data.provider
|
return globalSync.data.provider
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,13 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
preserveScroll(() => setTurnStart(nextStart))
|
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. */
|
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||||
const loadAndReveal = async () => {
|
const loadAndReveal = async () => {
|
||||||
const id = input.sessionID()
|
const id = input.sessionID()
|
||||||
|
|
@ -303,6 +310,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
return {
|
return {
|
||||||
turnStart,
|
turnStart,
|
||||||
setTurnStart,
|
setTurnStart,
|
||||||
|
reveal,
|
||||||
renderedUserMessages,
|
renderedUserMessages,
|
||||||
loadAndReveal,
|
loadAndReveal,
|
||||||
onScrollerScroll,
|
onScrollerScroll,
|
||||||
|
|
@ -877,6 +885,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||||
|
const wantsDiff = createMemo(() => (isDesktop() ? desktopReviewOpen() && activeTab() === "review" : mobileChanges()))
|
||||||
|
|
||||||
const fileTreeTab = () => layout.fileTree.tab()
|
const fileTreeTab = () => layout.fileTree.tab()
|
||||||
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
||||||
|
|
@ -1074,6 +1083,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusReviewDiff = (path: string) => {
|
const focusReviewDiff = (path: string) => {
|
||||||
|
void tabs().open("review")
|
||||||
openReviewPanel()
|
openReviewPanel()
|
||||||
view().review.openPath(path)
|
view().review.openPath(path)
|
||||||
setTree({ activeDiff: path, pendingDiff: path })
|
setTree({ activeDiff: path, pendingDiff: path })
|
||||||
|
|
@ -1124,10 +1134,7 @@ export default function Page() {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
const wants = isDesktop()
|
if (!wantsDiff()) return
|
||||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
|
||||||
: store.mobileTab === "changes"
|
|
||||||
if (!wants) return
|
|
||||||
if (sync.data.session_diff[id] !== undefined) return
|
if (sync.data.session_diff[id] !== undefined) return
|
||||||
if (sync.status === "loading") return
|
if (sync.status === "loading") return
|
||||||
|
|
||||||
|
|
@ -1136,13 +1143,7 @@ export default function Page() {
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() =>
|
() => [sessionKey(), wantsDiff()] as const,
|
||||||
[
|
|
||||||
sessionKey(),
|
|
||||||
isDesktop()
|
|
||||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
|
||||||
: store.mobileTab === "changes",
|
|
||||||
] as const,
|
|
||||||
([key, wants]) => {
|
([key, wants]) => {
|
||||||
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
||||||
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
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(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => sdk.directory,
|
() => sdk.directory,
|
||||||
|
|
@ -1296,9 +1284,9 @@ export default function Page() {
|
||||||
const el = scroller
|
const el = scroller
|
||||||
if (!el) return
|
if (!el) return
|
||||||
if (el.scrollHeight > el.clientHeight + 1) 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,
|
params.id,
|
||||||
messagesReady(),
|
messagesReady(),
|
||||||
historyWindow.turnStart(),
|
historyWindow.turnStart(),
|
||||||
historyMore(),
|
|
||||||
historyLoading(),
|
historyLoading(),
|
||||||
autoScroll.userScrolled(),
|
autoScroll.userScrolled(),
|
||||||
visibleUserMessages().length,
|
visibleUserMessages().length,
|
||||||
] as const,
|
] as const,
|
||||||
([id, ready, start, more, loading, scrolled]) => {
|
([id, ready, start, loading, scrolled]) => {
|
||||||
if (!id || !ready || loading || scrolled) return
|
if (!id || !ready || loading || scrolled) return
|
||||||
if (start <= 0 && !more) return
|
if (start <= 0) return
|
||||||
fill()
|
fill()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||||
const first = openedTabs()[0]
|
const first = openedTabs()[0]
|
||||||
if (first) return first
|
if (first) return first
|
||||||
if (contextOpen()) return "context"
|
if (contextOpen()) return "context"
|
||||||
if (review() && hasReview()) return "review"
|
|
||||||
return "empty"
|
return "empty"
|
||||||
})
|
})
|
||||||
const activeFileTab = createMemo(() => {
|
const activeFileTab = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
|
||||||
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
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 info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
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 reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
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(() => {
|
const reviewEmptyKey = createMemo(() => {
|
||||||
if (sync.project && !sync.project.vcs) return "session.review.noVcs"
|
if (sync.project && !sync.project.vcs) return "session.review.noVcs"
|
||||||
|
|
@ -71,7 +71,7 @@ export function SessionSidePanel(props: {
|
||||||
return "session.review.noChanges"
|
return "session.review.noChanges"
|
||||||
})
|
})
|
||||||
|
|
||||||
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
const diffFiles = createMemo(() => changes().map((d) => d.file))
|
||||||
const kinds = createMemo(() => {
|
const kinds = createMemo(() => {
|
||||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||||
if (!a) return b
|
if (!a) return b
|
||||||
|
|
@ -82,7 +82,7 @@ export function SessionSidePanel(props: {
|
||||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
||||||
|
|
||||||
const out = new Map<string, "add" | "del" | "mix">()
|
const out = new Map<string, "add" | "del" | "mix">()
|
||||||
for (const diff of diffs()) {
|
for (const diff of changes()) {
|
||||||
const file = normalize(diff.file)
|
const file = normalize(diff.file)
|
||||||
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||||
|
|
||||||
|
|
@ -293,9 +293,11 @@ export function SessionSidePanel(props: {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
iconSize="large"
|
iconSize="large"
|
||||||
class="!rounded-md"
|
class="!rounded-md"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
void import("@/components/dialog-select-file").then((x) =>
|
||||||
}
|
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />),
|
||||||
|
)
|
||||||
|
}}
|
||||||
aria-label={language.t("command.file.open")}
|
aria-label={language.t("command.file.open")}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
@ -386,26 +388,17 @@ export function SessionSidePanel(props: {
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={hasReview()}>
|
<Match when={hasReview() && diffFiles().length > 0}>
|
||||||
<Show
|
<FileTree
|
||||||
when={diffsReady()}
|
synthetic
|
||||||
fallback={
|
path=""
|
||||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
class="pt-3"
|
||||||
{language.t("common.loading")}
|
allowed={diffFiles()}
|
||||||
{language.t("common.loading.ellipsis")}
|
kinds={kinds()}
|
||||||
</div>
|
draggable={false}
|
||||||
}
|
active={props.activeDiff}
|
||||||
>
|
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||||
<FileTree
|
/>
|
||||||
path=""
|
|
||||||
class="pt-3"
|
|
||||||
allowed={diffFiles()}
|
|
||||||
kinds={kinds()}
|
|
||||||
draggable={false}
|
|
||||||
active={props.activeDiff}
|
|
||||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
{empty(
|
{empty(
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
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 { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { findLast } from "@opencode-ai/util/array"
|
import { findLast } from "@opencode-ai/util/array"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
|
|
@ -257,7 +253,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
description: language.t("palette.search.placeholder"),
|
description: language.t("palette.search.placeholder"),
|
||||||
keybind: "mod+k,mod+p",
|
keybind: "mod+k,mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
onSelect: () => {
|
||||||
|
void import("@/components/dialog-select-file").then((x) =>
|
||||||
|
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />),
|
||||||
|
)
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
fileCommand({
|
fileCommand({
|
||||||
id: "tab.close",
|
id: "tab.close",
|
||||||
|
|
@ -351,7 +351,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
description: language.t("command.model.choose.description"),
|
description: language.t("command.model.choose.description"),
|
||||||
keybind: "mod+'",
|
keybind: "mod+'",
|
||||||
slash: "model",
|
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({
|
mcpCommand({
|
||||||
id: "mcp.toggle",
|
id: "mcp.toggle",
|
||||||
|
|
@ -359,7 +363,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
description: language.t("command.mcp.toggle.description"),
|
description: language.t("command.mcp.toggle.description"),
|
||||||
keybind: "mod+;",
|
keybind: "mod+;",
|
||||||
slash: "mcp",
|
slash: "mcp",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
onSelect: () => {
|
||||||
|
void import("@/components/dialog-select-mcp").then((x) => dialog.show(() => <x.DialogSelectMcp />))
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
agentCommand({
|
agentCommand({
|
||||||
id: "agent.cycle",
|
id: "agent.cycle",
|
||||||
|
|
@ -487,7 +493,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
description: language.t("command.session.fork.description"),
|
description: language.t("command.session.fork.description"),
|
||||||
slash: "fork",
|
slash: "fork",
|
||||||
disabled: !params.id || visibleUserMessages().length === 0,
|
disabled: !params.id || visibleUserMessages().length === 0,
|
||||||
onSelect: () => dialog.show(() => <DialogFork />),
|
onSelect: () => {
|
||||||
|
void import("@/components/dialog-fork").then((x) => dialog.show(() => <x.DialogFork />))
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
...share,
|
...share,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,15 @@ export const SessionRoutes = lazy(() =>
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
log.info("SEARCH", { url: c.req.url })
|
log.info("SEARCH", { url: c.req.url })
|
||||||
const session = await Session.get(sessionID)
|
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)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,58 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
const csp = (hash = "") =>
|
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:`
|
`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 {
|
export namespace Server {
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
|
|
@ -104,6 +156,26 @@ export namespace Server {
|
||||||
timer.stop()
|
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(
|
.use(
|
||||||
cors({
|
cors({
|
||||||
origin(input) {
|
origin(input) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ import { Bus } from "@/bus"
|
||||||
import { NotFoundError } from "@/storage/db"
|
import { NotFoundError } from "@/storage/db"
|
||||||
|
|
||||||
export namespace SessionSummary {
|
export namespace SessionSummary {
|
||||||
|
function shape(diffs: Snapshot.FileDiff[]) {
|
||||||
|
return diffs.map((item) => ({
|
||||||
|
...item,
|
||||||
|
before: "",
|
||||||
|
after: "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
function unquoteGitPath(input: string) {
|
function unquoteGitPath(input: string) {
|
||||||
if (!input.startsWith('"')) return input
|
if (!input.startsWith('"')) return input
|
||||||
if (!input.endsWith('"')) 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[] }) {
|
export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
|
||||||
let from: string | undefined
|
let from: string | undefined
|
||||||
let to: string | undefined
|
let to: string | undefined
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,31 @@ import { type Config } from "./gen/client/types.gen.js"
|
||||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||||
export { type Config as OpencodeClientConfig, OpencodeClient }
|
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 }) {
|
export function createOpencodeClient(config?: Config & { directory?: string }) {
|
||||||
if (!config?.fetch) {
|
if (!config?.fetch) {
|
||||||
const customFetch: any = (req: any) => {
|
const customFetch: any = (req: any) => {
|
||||||
|
|
@ -26,5 +51,8 @@ export function createOpencodeClient(config?: Config & { directory?: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createClient(config)
|
const client = createClient(config)
|
||||||
|
if (typeof window === "object" && typeof document === "object") {
|
||||||
|
client.interceptors.request.use(move)
|
||||||
|
}
|
||||||
return new OpencodeClient({ client })
|
return new OpencodeClient({ client })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,31 @@ import { type Config } from "./gen/client/types.gen.js"
|
||||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||||
export { type Config as OpencodeClientConfig, OpencodeClient }
|
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 }) {
|
export function createOpencodeClient(config?: Config & { directory?: string; experimental_workspaceID?: string }) {
|
||||||
if (!config?.fetch) {
|
if (!config?.fetch) {
|
||||||
const customFetch: any = (req: any) => {
|
const customFetch: any = (req: any) => {
|
||||||
|
|
@ -35,5 +60,8 @@ export function createOpencodeClient(config?: Config & { directory?: string; exp
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createClient(config)
|
const client = createClient(config)
|
||||||
|
if (typeof window === "object" && typeof document === "object") {
|
||||||
|
client.interceptors.request.use(move)
|
||||||
|
}
|
||||||
return new OpencodeClient({ client })
|
return new OpencodeClient({ client })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ibmPlexMonoMedium from "../assets/fonts/ibm-plex-mono-medium.woff2"
|
||||||
import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2"
|
import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2"
|
||||||
|
|
||||||
export const Font = () => {
|
export const Font = (props: { preloadMono?: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Style>{`
|
<Style>{`
|
||||||
|
|
@ -56,7 +56,9 @@ export const Font = () => {
|
||||||
`}</Style>
|
`}</Style>
|
||||||
<Show when={typeof location === "undefined" || location.protocol !== "file:"}>
|
<Show when={typeof location === "undefined" || location.protocol !== "file:"}>
|
||||||
<Link rel="preload" href={inter} as="font" type="font/woff2" crossorigin="anonymous" />
|
<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>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue