fix(app): more startup efficiency (#19454)

This commit is contained in:
Adam
2026-03-28 01:26:57 +00:00
committed by GitHub
parent 82fc493520
commit f736116967
19 changed files with 251 additions and 205 deletions
+24 -24
View File
@@ -159,7 +159,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
const dialog = await openSettings(page) const dialog = await openSettings(page)
const input = dialog.locator(settingsCodeFontSelector) const input = dialog.locator(settingsCodeFontSelector)
await expect(input).toBeVisible() await expect(input).toBeVisible()
await expect(input).toHaveAttribute("placeholder", "IBM Plex Mono") await expect(input).toHaveAttribute("placeholder", "System Mono")
const initialFontFamily = await page.evaluate(() => const initialFontFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
@@ -167,7 +167,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
const initialUIFamily = await page.evaluate(() => const initialUIFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
) )
expect(initialFontFamily).toContain("IBM Plex Mono") expect(initialFontFamily).toContain("ui-monospace")
const next = "Test Mono" const next = "Test Mono"
@@ -185,7 +185,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
font: next, mono: next,
}, },
}) })
@@ -206,7 +206,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
const dialog = await openSettings(page) const dialog = await openSettings(page)
const input = dialog.locator(settingsUIFontSelector) const input = dialog.locator(settingsUIFontSelector)
await expect(input).toBeVisible() await expect(input).toBeVisible()
await expect(input).toHaveAttribute("placeholder", "Inter") await expect(input).toHaveAttribute("placeholder", "System Sans")
const initialFontFamily = await page.evaluate(() => const initialFontFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
@@ -214,7 +214,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
const initialCodeFamily = await page.evaluate(() => const initialCodeFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
) )
expect(initialFontFamily).toContain("Inter") expect(initialFontFamily).toContain("ui-sans-serif")
const next = "Test Sans" const next = "Test Sans"
@@ -232,7 +232,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
uiFont: next, sans: next,
}, },
}) })
@@ -267,14 +267,14 @@ test("clearing the code font field restores the default placeholder and stack",
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
font: "Reset Mono", mono: "Reset Mono",
}, },
}) })
await input.clear() await input.clear()
await input.press("Space") await input.press("Space")
await expect(input).toHaveValue("") await expect(input).toHaveValue("")
await expect(input).toHaveAttribute("placeholder", "IBM Plex Mono") await expect(input).toHaveAttribute("placeholder", "System Mono")
await expect await expect
.poll(async () => { .poll(async () => {
@@ -285,14 +285,14 @@ test("clearing the code font field restores the default placeholder and stack",
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
font: "", mono: "",
}, },
}) })
const fontFamily = await page.evaluate(() => const fontFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(), getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
) )
expect(fontFamily).toContain("IBM Plex Mono") expect(fontFamily).toContain("ui-monospace")
expect(fontFamily).not.toContain("Reset Mono") expect(fontFamily).not.toContain("Reset Mono")
}) })
@@ -316,14 +316,14 @@ test("clearing the UI font field restores the default placeholder and stack", as
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
uiFont: "Reset Sans", sans: "Reset Sans",
}, },
}) })
await input.clear() await input.clear()
await input.press("Space") await input.press("Space")
await expect(input).toHaveValue("") await expect(input).toHaveValue("")
await expect(input).toHaveAttribute("placeholder", "Inter") await expect(input).toHaveAttribute("placeholder", "System Sans")
await expect await expect
.poll(async () => { .poll(async () => {
@@ -334,14 +334,14 @@ test("clearing the UI font field restores the default placeholder and stack", as
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
uiFont: "", sans: "",
}, },
}) })
const fontFamily = await page.evaluate(() => const fontFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(), getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
) )
expect(fontFamily).toContain("Inter") expect(fontFamily).toContain("ui-sans-serif")
expect(fontFamily).not.toContain("Reset Sans") expect(fontFamily).not.toContain("Reset Sans")
}) })
@@ -373,8 +373,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
return raw ? JSON.parse(raw) : null return raw ? JSON.parse(raw) : null
}, settingsKey) }, settingsKey)
const mono = initialSettings?.appearance?.font === "Reload Mono" ? "Reload Mono 2" : "Reload Mono" const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
const sans = initialSettings?.appearance?.uiFont === "Reload Sans" ? "Reload Sans 2" : "Reload Sans" const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
await code.click() await code.click()
await code.clear() await code.clear()
@@ -395,8 +395,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
font: mono, mono,
uiFont: sans, sans,
}, },
}) })
@@ -415,8 +415,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
expect(updatedMono).not.toBe(initialMono) expect(updatedMono).not.toBe(initialMono)
expect(updatedSans).toContain(sans) expect(updatedSans).toContain(sans)
expect(updatedSans).not.toBe(initialSans) expect(updatedSans).not.toBe(initialSans)
expect(updatedSettings?.appearance?.font).toBe(mono) expect(updatedSettings?.appearance?.mono).toBe(mono)
expect(updatedSettings?.appearance?.uiFont).toBe(sans) expect(updatedSettings?.appearance?.sans).toBe(sans)
await closeDialog(page, dialog) await closeDialog(page, dialog)
await page.reload() await page.reload()
@@ -432,8 +432,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
}) })
.toMatchObject({ .toMatchObject({
appearance: { appearance: {
font: mono, mono,
uiFont: sans, sans,
}, },
}) })
@@ -468,8 +468,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
expect(rehydratedMono).not.toBe(initialMono) expect(rehydratedMono).not.toBe(initialMono)
expect(rehydratedSans).toContain(sans) expect(rehydratedSans).toContain(sans)
expect(rehydratedSans).not.toBe(initialSans) expect(rehydratedSans).not.toBe(initialSans)
expect(rehydratedSettings?.appearance?.font).toBe(mono) expect(rehydratedSettings?.appearance?.mono).toBe(mono)
expect(rehydratedSettings?.appearance?.uiFont).toBe(sans) expect(rehydratedSettings?.appearance?.sans).toBe(sans)
}) })
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => { test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
+11 -2
View File
@@ -47,9 +47,14 @@ import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health" import { useCheckServerHealth } from "./utils/server-health"
const HomeRoute = lazy(() => import("@/pages/home")) const HomeRoute = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session")) const loadSession = () => import("@/pages/session")
const Session = lazy(loadSession)
const Loading = () => <div class="size-full" /> const Loading = () => <div class="size-full" />
if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) {
void loadSession()
}
const SessionRoute = () => ( const SessionRoute = () => (
<SessionProviders> <SessionProviders>
<Session /> <Session />
@@ -278,7 +283,11 @@ export function AppInterface(props: {
disableHealthCheck?: boolean disableHealthCheck?: boolean
}) { }) {
return ( return (
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}> <ServerProvider
defaultServer={props.defaultServer}
disableHealthCheck={props.disableHealthCheck}
servers={props.servers}
>
<ConnectionGate disableHealthCheck={props.disableHealthCheck}> <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
<ServerKey> <ServerKey>
<GlobalSDKProvider> <GlobalSDKProvider>
+82 -60
View File
@@ -105,6 +105,8 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
const aborted = (error: unknown) => abortError.safeParse(error).success const aborted = (error: unknown) => abortError.safeParse(error).success
let attempt: AbortController | undefined let attempt: AbortController | undefined
let run: Promise<void> | undefined
let started = false
const HEARTBEAT_TIMEOUT_MS = 15_000 const HEARTBEAT_TIMEOUT_MS = 15_000
let lastEventAt = Date.now() let lastEventAt = Date.now()
let heartbeat: ReturnType<typeof setTimeout> | undefined let heartbeat: ReturnType<typeof setTimeout> | undefined
@@ -121,78 +123,93 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
heartbeat = undefined heartbeat = undefined
} }
void (async () => { const start = () => {
while (!abort.signal.aborted) { if (started) return run
attempt = new AbortController() started = true
lastEventAt = Date.now() run = (async () => {
const onAbort = () => { while (!abort.signal.aborted && started) {
attempt?.abort() attempt = new AbortController()
} lastEventAt = Date.now()
abort.signal.addEventListener("abort", onAbort) const onAbort = () => {
try { attempt?.abort()
const events = await eventSdk.global.event({ }
signal: attempt.signal, abort.signal.addEventListener("abort", onAbort)
onSseError: (error) => { try {
if (aborted(error)) return const events = await eventSdk.global.event({
if (streamErrorLogged) return signal: attempt.signal,
onSseError: (error) => {
if (aborted(error)) return
if (streamErrorLogged) return
streamErrorLogged = true
console.error("[global-sdk] event stream error", {
url: currentServer.http.url,
fetch: eventFetch ? "platform" : "webview",
error,
})
},
})
let yielded = Date.now()
resetHeartbeat()
for await (const event of events.stream) {
resetHeartbeat()
streamErrorLogged = false
const directory = event.directory ?? "global"
const payload = event.payload
const k = key(directory, payload)
if (k) {
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = { directory, payload }
if (payload.type === "message.part.updated") {
const part = payload.properties.part
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
}
continue
}
coalesced.set(k, queue.length)
}
queue.push({ directory, payload })
schedule()
if (Date.now() - yielded < STREAM_YIELD_MS) continue
yielded = Date.now()
await wait(0)
}
} catch (error) {
if (!aborted(error) && !streamErrorLogged) {
streamErrorLogged = true streamErrorLogged = true
console.error("[global-sdk] event stream error", { console.error("[global-sdk] event stream failed", {
url: currentServer.http.url, url: currentServer.http.url,
fetch: eventFetch ? "platform" : "webview", fetch: eventFetch ? "platform" : "webview",
error, error,
}) })
},
})
let yielded = Date.now()
resetHeartbeat()
for await (const event of events.stream) {
resetHeartbeat()
streamErrorLogged = false
const directory = event.directory ?? "global"
const payload = event.payload
const k = key(directory, payload)
if (k) {
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = { directory, payload }
if (payload.type === "message.part.updated") {
const part = payload.properties.part
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
}
continue
}
coalesced.set(k, queue.length)
} }
queue.push({ directory, payload }) } finally {
schedule() abort.signal.removeEventListener("abort", onAbort)
attempt = undefined
clearHeartbeat()
}
if (Date.now() - yielded < STREAM_YIELD_MS) continue if (abort.signal.aborted || !started) return
yielded = Date.now() await wait(RECONNECT_DELAY_MS)
await wait(0)
}
} catch (error) {
if (!aborted(error) && !streamErrorLogged) {
streamErrorLogged = true
console.error("[global-sdk] event stream failed", {
url: currentServer.http.url,
fetch: eventFetch ? "platform" : "webview",
error,
})
}
} finally {
abort.signal.removeEventListener("abort", onAbort)
attempt = undefined
clearHeartbeat()
} }
})().finally(() => {
run = undefined
flush()
})
return run
}
if (abort.signal.aborted) return const stop = () => {
await wait(RECONNECT_DELAY_MS) started = false
} attempt?.abort()
})().finally(flush) clearHeartbeat()
}
const onVisibility = () => { const onVisibility = () => {
if (typeof document === "undefined") return if (typeof document === "undefined") return
if (document.visibilityState !== "visible") return if (document.visibilityState !== "visible") return
if (!started) return
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
attempt?.abort() attempt?.abort()
} }
@@ -204,6 +221,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibility) document.removeEventListener("visibilitychange", onVisibility)
} }
stop()
abort.abort() abort.abort()
flush() flush()
}) })
@@ -217,7 +235,11 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
return { return {
url: currentServer.http.url, url: currentServer.http.url,
client: sdk, client: sdk,
event: emitter, event: {
on: emitter.on.bind(emitter),
listen: emitter.listen.bind(emitter),
start,
},
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) { createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
const s = server.current const s = server.current
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable")) if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
+20
View File
@@ -72,10 +72,16 @@ function createGlobalSync() {
let projectWritten = false let projectWritten = false
let bootedAt = 0 let bootedAt = 0
let bootingRoot = false let bootingRoot = false
let eventFrame: number | undefined
let eventTimer: ReturnType<typeof setTimeout> | undefined
onCleanup(() => { onCleanup(() => {
active = false active = false
}) })
onCleanup(() => {
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
if (eventTimer !== undefined) clearTimeout(eventTimer)
})
const cacheProjects = () => { const cacheProjects = () => {
setProjectCache( setProjectCache(
@@ -348,6 +354,20 @@ function createGlobalSync() {
} }
onMount(() => { onMount(() => {
if (typeof requestAnimationFrame === "function") {
eventFrame = requestAnimationFrame(() => {
eventFrame = undefined
eventTimer = setTimeout(() => {
eventTimer = undefined
globalSDK.event.start()
}, 0)
})
} else {
eventTimer = setTimeout(() => {
eventTimer = undefined
globalSDK.event.start()
}, 0)
}
void bootstrap() void bootstrap()
}) })
@@ -43,8 +43,10 @@ function waitForPaint() {
const timer = setTimeout(finish, 50) const timer = setTimeout(finish, 50)
if (typeof requestAnimationFrame !== "function") return if (typeof requestAnimationFrame !== "function") return
requestAnimationFrame(() => { requestAnimationFrame(() => {
clearTimeout(timer) setTimeout(() => {
finish() clearTimeout(timer)
finish()
}, 0)
}) })
}) })
} }
@@ -87,12 +89,6 @@ export async function bootstrapGlobal(input: {
setGlobalStore: SetStoreFunction<GlobalStore> setGlobalStore: SetStoreFunction<GlobalStore>
}) { }) {
const fast = [ const fast = [
() =>
retry(() =>
input.globalSDK.path.get().then((x) => {
input.setGlobalStore("path", x.data!)
}),
),
() => () =>
retry(() => retry(() =>
input.globalSDK.global.config.get().then((x) => { input.globalSDK.global.config.get().then((x) => {
@@ -108,6 +104,12 @@ export async function bootstrapGlobal(input: {
] ]
const slow = [ const slow = [
() =>
retry(() =>
input.globalSDK.path.get().then((x) => {
input.setGlobalStore("path", x.data!)
}),
),
() => () =>
retry(() => retry(() =>
input.globalSDK.project.list().then((x) => { input.globalSDK.project.list().then((x) => {
@@ -221,12 +223,16 @@ export async function bootstrapDirectory(input: {
if (loading) input.setStore("status", "partial") if (loading) input.setStore("status", "partial")
const fast = [ const fast = [
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
]
const slow = [
() => () =>
seededProject seededProject
? Promise.resolve() ? Promise.resolve()
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
() => () =>
seededPath seededPath
? Promise.resolve() ? Promise.resolve()
@@ -237,7 +243,6 @@ export async function bootstrapDirectory(input: {
if (next) input.setStore("project", next) if (next) input.setStore("project", next)
}), }),
), ),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
() => () =>
retry(() => retry(() =>
input.sdk.vcs.get().then((x) => { input.sdk.vcs.get().then((x) => {
@@ -299,9 +304,6 @@ export async function bootstrapDirectory(input: {
) )
}), }),
), ),
]
const slow = [
() => Promise.resolve(input.loadSessions(input.directory)), () => Promise.resolve(input.loadSessions(input.directory)),
() => () =>
retry(() => retry(() =>
+19 -5
View File
@@ -544,12 +544,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
}) })
let sessionFrame: number | undefined
let sessionTimer: number | undefined
onMount(() => { onMount(() => {
Promise.all( sessionFrame = requestAnimationFrame(() => {
server.projects.list().map((project) => { sessionFrame = undefined
return globalSync.project.loadSessions(project.worktree) sessionTimer = window.setTimeout(() => {
}), sessionTimer = undefined
) void Promise.all(
server.projects.list().map((project) => {
return globalSync.project.loadSessions(project.worktree)
}),
)
}, 0)
})
})
onCleanup(() => {
if (sessionFrame !== undefined) cancelAnimationFrame(sessionFrame)
if (sessionTimer !== undefined) window.clearTimeout(sessionTimer)
}) })
return { return {
+9 -1
View File
@@ -94,7 +94,11 @@ export namespace ServerConnection {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({ export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server", name: "Server",
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => { init: (props: {
defaultServer: ServerConnection.Key
disableHealthCheck?: boolean
servers?: Array<ServerConnection.Any>
}) => {
const checkServerHealth = useCheckServerHealth() const checkServerHealth = useCheckServerHealth()
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
@@ -202,6 +206,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const current_ = current() const current_ = current()
if (!current_) return if (!current_) return
if (props.disableHealthCheck) {
setState("healthy", true)
return
}
setState("healthy", undefined) setState("healthy", undefined)
onCleanup(startHealthPolling(current_)) onCleanup(startHealthPolling(current_))
}) })
+20 -22
View File
@@ -32,8 +32,8 @@ export interface Settings {
} }
appearance: { appearance: {
fontSize: number fontSize: number
font: string mono: string
uiFont: string sans: string
} }
keybinds: Record<string, string> keybinds: Record<string, string>
permissions: { permissions: {
@@ -43,20 +43,18 @@ export interface Settings {
sounds: SoundSettings sounds: SoundSettings
} }
export const monoDefault = "IBM Plex Mono" export const monoDefault = "System Mono"
export const sansDefault = "Inter" export const sansDefault = "System Sans"
const monoFallback = const monoFallback =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
const monoBase = `"${monoDefault}", "IBM Plex Mono Fallback", ${monoFallback}` const monoBase = monoFallback
const sansBase = `"${sansDefault}", "Inter Fallback", ${sansFallback}` const sansBase = sansFallback
const monoKey = "ibm-plex-mono"
function input(font: string | undefined, key?: string) { function input(font: string | undefined) {
if (!font || font === key || !font.trim()) return "" return font ?? ""
return font
} }
function family(font: string) { function family(font: string) {
@@ -64,14 +62,14 @@ function family(font: string) {
return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
} }
function stack(font: string | undefined, base: string, key?: string) { function stack(font: string | undefined, base: string) {
const value = input(font, key).trim() const value = font?.trim() ?? ""
if (!value) return base if (!value) return base
return `${family(value)}, ${base}` return `${family(value)}, ${base}`
} }
export function monoInput(font: string | undefined) { export function monoInput(font: string | undefined) {
return input(font, monoKey) return input(font)
} }
export function sansInput(font: string | undefined) { export function sansInput(font: string | undefined) {
@@ -79,7 +77,7 @@ export function sansInput(font: string | undefined) {
} }
export function monoFontFamily(font: string | undefined) { export function monoFontFamily(font: string | undefined) {
return stack(font, monoBase, monoKey) return stack(font, monoBase)
} }
export function sansFontFamily(font: string | undefined) { export function sansFontFamily(font: string | undefined) {
@@ -100,8 +98,8 @@ const defaultSettings: Settings = {
}, },
appearance: { appearance: {
fontSize: 14, fontSize: 14,
font: "", mono: "",
uiFont: "", sans: "",
}, },
keybinds: {}, keybinds: {},
permissions: { permissions: {
@@ -134,8 +132,8 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
createEffect(() => { createEffect(() => {
if (typeof document === "undefined") return if (typeof document === "undefined") return
const root = document.documentElement const root = document.documentElement
root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font)) root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.mono))
root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.uiFont)) root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans))
}) })
return { return {
@@ -189,13 +187,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setFontSize(value: number) { setFontSize(value: number) {
setStore("appearance", "fontSize", value) setStore("appearance", "fontSize", value)
}, },
font: withFallback(() => store.appearance?.font, defaultSettings.appearance.font), font: withFallback(() => store.appearance?.mono, defaultSettings.appearance.mono),
setFont(value: string) { setFont(value: string) {
setStore("appearance", "font", value.trim() ? value : "") setStore("appearance", "mono", value.trim() ? value : "")
}, },
uiFont: withFallback(() => store.appearance?.uiFont, defaultSettings.appearance.uiFont), uiFont: withFallback(() => store.appearance?.sans, defaultSettings.appearance.sans),
setUIFont(value: string) { setUIFont(value: string) {
setStore("appearance", "uiFont", value.trim() ? value : "") setStore("appearance", "sans", value.trim() ? value : "")
}, },
}, },
keybinds: { keybinds: {
+39 -2
View File
@@ -544,6 +544,8 @@ export default function Page() {
let reviewFrame: number | undefined let reviewFrame: number | undefined
let refreshFrame: number | undefined let refreshFrame: number | undefined
let refreshTimer: number | undefined let refreshTimer: number | undefined
let todoFrame: number | undefined
let todoTimer: number | undefined
let diffFrame: number | undefined let diffFrame: number | undefined
let diffTimer: number | undefined let diffTimer: number | undefined
@@ -718,7 +720,6 @@ export default function Page() {
if (!info) return true if (!info) return true
return Date.now() - info.at > SESSION_PREFETCH_TTL return Date.now() - info.at > SESSION_PREFETCH_TTL
})() })()
const todos = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
untrack(() => { untrack(() => {
void sync.session.sync(id) void sync.session.sync(id)
}) })
@@ -730,13 +731,47 @@ export default function Page() {
if (params.id !== id) return if (params.id !== id) return
untrack(() => { untrack(() => {
if (stale) void sync.session.sync(id, { force: true }) if (stale) void sync.session.sync(id, { force: true })
void sync.session.todo(id, todos ? { force: true } : undefined)
}) })
}, 0) }, 0)
}) })
}), }),
) )
createEffect(
on(
() => {
const id = params.id
return [
sdk.directory,
id,
id ? (sync.data.session_status[id]?.type ?? "idle") : "idle",
id ? composer.blocked() : false,
] as const
},
([dir, id, status, blocked]) => {
if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
if (todoTimer !== undefined) window.clearTimeout(todoTimer)
todoFrame = undefined
todoTimer = undefined
if (!id) return
if (status === "idle" && !blocked) return
const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
todoFrame = requestAnimationFrame(() => {
todoFrame = undefined
todoTimer = window.setTimeout(() => {
todoTimer = undefined
if (sdk.directory !== dir || params.id !== id) return
untrack(() => {
void sync.session.todo(id, cached ? { force: true } : undefined)
})
}, 0)
})
},
{ defer: true },
),
)
createEffect( createEffect(
on( on(
() => visibleUserMessages().at(-1)?.id, () => visibleUserMessages().at(-1)?.id,
@@ -1658,6 +1693,8 @@ export default function Page() {
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
if (todoTimer !== undefined) window.clearTimeout(todoTimer)
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
if (diffTimer !== undefined) window.clearTimeout(diffTimer) if (diffTimer !== undefined) window.clearTimeout(diffTimer)
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame) if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
@@ -1 +0,0 @@
BlexMonoNerdFontMono-Bold.woff2
@@ -1 +0,0 @@
BlexMonoNerdFontMono-Medium.woff2
@@ -1 +0,0 @@
BlexMonoNerdFontMono-Regular.woff2
Binary file not shown.
+5 -5
View File
@@ -2,24 +2,24 @@
import * as mod from "./font" import * as mod from "./font"
const docs = `### Overview const docs = `### Overview
Loads OpenCode typography assets and mono nerd fonts. Uses native system font stacks for sans and mono typography.
Render once at the app root or Storybook preview. Optional compatibility component. Existing roots can keep rendering it, but it does nothing.
### API ### API
- No props. - No props.
### Variants and states ### Variants and states
- Fonts include sans and multiple mono families. - No variants.
### Behavior ### Behavior
- Injects @font-face rules and preload links into the document head. - Compatibility wrapper only. No font assets are injected or preloaded.
### Accessibility ### Accessibility
- Not applicable. - Not applicable.
### Theming/tokens ### Theming/tokens
- Provides font families used by theme tokens. - Theme tokens come from CSS variables, not this component.
` `
+1 -63
View File
@@ -1,63 +1 @@
import { Link, Style } from "@solidjs/meta" export const Font = () => null
import { Show } from "solid-js"
import inter from "../assets/fonts/inter.woff2"
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 = () => {
return (
<>
<Style>{`
@font-face {
font-family: "Inter";
src: url("${inter}") format("woff2-variations");
font-display: swap;
font-style: normal;
font-weight: 100 900;
}
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 100%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 1%;
}
@font-face {
font-family: "IBM Plex Mono";
src: url("${ibmPlexMonoRegular}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "IBM Plex Mono";
src: url("${ibmPlexMonoMedium}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 500;
}
@font-face {
font-family: "IBM Plex Mono";
src: url("${ibmPlexMonoBold}") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "IBM Plex Mono Fallback";
src: local("Courier New");
size-adjust: 100%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 1%;
}
`}</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>
</>
)
}
+5 -4
View File
@@ -1,8 +1,9 @@
:root { :root {
--font-family-sans: "Inter", "Inter Fallback"; --font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-family-sans--font-feature-settings: "ss03" 1; --font-family-sans--font-feature-settings: normal;
--font-family-mono: "IBM Plex Mono", "IBM Plex Mono Fallback"; --font-family-mono:
--font-family-mono--font-feature-settings: "ss01" 1; ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-mono--font-feature-settings: normal;
--font-size-small: 13px; --font-size-small: 13px;
--font-size-base: 14px; --font-size-base: 14px;