diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts
index 0602e95c7e..1b151b6066 100644
--- a/packages/app/e2e/settings/settings.spec.ts
+++ b/packages/app/e2e/settings/settings.spec.ts
@@ -159,7 +159,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
const dialog = await openSettings(page)
const input = dialog.locator(settingsCodeFontSelector)
await expect(input).toBeVisible()
- await expect(input).toHaveAttribute("placeholder", "IBM Plex Mono")
+ await expect(input).toHaveAttribute("placeholder", "System Mono")
const initialFontFamily = await page.evaluate(() =>
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(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
)
- expect(initialFontFamily).toContain("IBM Plex Mono")
+ expect(initialFontFamily).toContain("ui-monospace")
const next = "Test Mono"
@@ -185,7 +185,7 @@ test("typing a code font with spaces persists and updates CSS variable", async (
})
.toMatchObject({
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 input = dialog.locator(settingsUIFontSelector)
await expect(input).toBeVisible()
- await expect(input).toHaveAttribute("placeholder", "Inter")
+ await expect(input).toHaveAttribute("placeholder", "System Sans")
const initialFontFamily = await page.evaluate(() =>
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(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
)
- expect(initialFontFamily).toContain("Inter")
+ expect(initialFontFamily).toContain("ui-sans-serif")
const next = "Test Sans"
@@ -232,7 +232,7 @@ test("typing a UI font with spaces persists and updates CSS variable", async ({
})
.toMatchObject({
appearance: {
- uiFont: next,
+ sans: next,
},
})
@@ -267,14 +267,14 @@ test("clearing the code font field restores the default placeholder and stack",
})
.toMatchObject({
appearance: {
- font: "Reset Mono",
+ mono: "Reset Mono",
},
})
await input.clear()
await input.press("Space")
await expect(input).toHaveValue("")
- await expect(input).toHaveAttribute("placeholder", "IBM Plex Mono")
+ await expect(input).toHaveAttribute("placeholder", "System Mono")
await expect
.poll(async () => {
@@ -285,14 +285,14 @@ test("clearing the code font field restores the default placeholder and stack",
})
.toMatchObject({
appearance: {
- font: "",
+ mono: "",
},
})
const fontFamily = await page.evaluate(() =>
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")
})
@@ -316,14 +316,14 @@ test("clearing the UI font field restores the default placeholder and stack", as
})
.toMatchObject({
appearance: {
- uiFont: "Reset Sans",
+ sans: "Reset Sans",
},
})
await input.clear()
await input.press("Space")
await expect(input).toHaveValue("")
- await expect(input).toHaveAttribute("placeholder", "Inter")
+ await expect(input).toHaveAttribute("placeholder", "System Sans")
await expect
.poll(async () => {
@@ -334,14 +334,14 @@ test("clearing the UI font field restores the default placeholder and stack", as
})
.toMatchObject({
appearance: {
- uiFont: "",
+ sans: "",
},
})
const fontFamily = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
)
- expect(fontFamily).toContain("Inter")
+ expect(fontFamily).toContain("ui-sans-serif")
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
}, settingsKey)
- const mono = initialSettings?.appearance?.font === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
- const sans = initialSettings?.appearance?.uiFont === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
+ const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
+ const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
await code.click()
await code.clear()
@@ -395,8 +395,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
})
.toMatchObject({
appearance: {
- font: mono,
- uiFont: sans,
+ mono,
+ sans,
},
})
@@ -415,8 +415,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
expect(updatedMono).not.toBe(initialMono)
expect(updatedSans).toContain(sans)
expect(updatedSans).not.toBe(initialSans)
- expect(updatedSettings?.appearance?.font).toBe(mono)
- expect(updatedSettings?.appearance?.uiFont).toBe(sans)
+ expect(updatedSettings?.appearance?.mono).toBe(mono)
+ expect(updatedSettings?.appearance?.sans).toBe(sans)
await closeDialog(page, dialog)
await page.reload()
@@ -432,8 +432,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
})
.toMatchObject({
appearance: {
- font: mono,
- uiFont: sans,
+ mono,
+ sans,
},
})
@@ -468,8 +468,8 @@ test("color scheme, code font, and UI font rehydrate after reload", async ({ pag
expect(rehydratedMono).not.toBe(initialMono)
expect(rehydratedSans).toContain(sans)
expect(rehydratedSans).not.toBe(initialSans)
- expect(rehydratedSettings?.appearance?.font).toBe(mono)
- expect(rehydratedSettings?.appearance?.uiFont).toBe(sans)
+ expect(rehydratedSettings?.appearance?.mono).toBe(mono)
+ expect(rehydratedSettings?.appearance?.sans).toBe(sans)
})
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index a248ebb944..eb3c582d29 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -47,9 +47,14 @@ import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health"
const HomeRoute = lazy(() => import("@/pages/home"))
-const Session = lazy(() => import("@/pages/session"))
+const loadSession = () => import("@/pages/session")
+const Session = lazy(loadSession)
const Loading = () =>
+if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) {
+ void loadSession()
+}
+
const SessionRoute = () => (
@@ -278,7 +283,11 @@ export function AppInterface(props: {
disableHealthCheck?: boolean
}) {
return (
-
+
diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx
index 60e9fd6d54..d240f9eeff 100644
--- a/packages/app/src/context/global-sdk.tsx
+++ b/packages/app/src/context/global-sdk.tsx
@@ -105,6 +105,8 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
const aborted = (error: unknown) => abortError.safeParse(error).success
let attempt: AbortController | undefined
+ let run: Promise | undefined
+ let started = false
const HEARTBEAT_TIMEOUT_MS = 15_000
let lastEventAt = Date.now()
let heartbeat: ReturnType | undefined
@@ -121,78 +123,93 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
heartbeat = undefined
}
- void (async () => {
- while (!abort.signal.aborted) {
- attempt = new AbortController()
- lastEventAt = Date.now()
- const onAbort = () => {
- attempt?.abort()
- }
- abort.signal.addEventListener("abort", onAbort)
- try {
- const events = await eventSdk.global.event({
- signal: attempt.signal,
- onSseError: (error) => {
- if (aborted(error)) return
- if (streamErrorLogged) return
+ const start = () => {
+ if (started) return run
+ started = true
+ run = (async () => {
+ while (!abort.signal.aborted && started) {
+ attempt = new AbortController()
+ lastEventAt = Date.now()
+ const onAbort = () => {
+ attempt?.abort()
+ }
+ abort.signal.addEventListener("abort", onAbort)
+ try {
+ const events = await eventSdk.global.event({
+ 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
- console.error("[global-sdk] event stream error", {
+ console.error("[global-sdk] event stream failed", {
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()
+ } finally {
+ abort.signal.removeEventListener("abort", onAbort)
+ attempt = undefined
+ clearHeartbeat()
+ }
- if (Date.now() - yielded < STREAM_YIELD_MS) continue
- yielded = Date.now()
- 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()
+ if (abort.signal.aborted || !started) return
+ await wait(RECONNECT_DELAY_MS)
}
+ })().finally(() => {
+ run = undefined
+ flush()
+ })
+ return run
+ }
- if (abort.signal.aborted) return
- await wait(RECONNECT_DELAY_MS)
- }
- })().finally(flush)
+ const stop = () => {
+ started = false
+ attempt?.abort()
+ clearHeartbeat()
+ }
const onVisibility = () => {
if (typeof document === "undefined") return
if (document.visibilityState !== "visible") return
+ if (!started) return
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
attempt?.abort()
}
@@ -204,6 +221,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibility)
}
+ stop()
abort.abort()
flush()
})
@@ -217,7 +235,11 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
return {
url: currentServer.http.url,
client: sdk,
- event: emitter,
+ event: {
+ on: emitter.on.bind(emitter),
+ listen: emitter.listen.bind(emitter),
+ start,
+ },
createClient(opts: Omit[0], "server" | "fetch">) {
const s = server.current
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index 86ac9b45a0..0cf3570a8b 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -72,10 +72,16 @@ function createGlobalSync() {
let projectWritten = false
let bootedAt = 0
let bootingRoot = false
+ let eventFrame: number | undefined
+ let eventTimer: ReturnType | undefined
onCleanup(() => {
active = false
})
+ onCleanup(() => {
+ if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
+ if (eventTimer !== undefined) clearTimeout(eventTimer)
+ })
const cacheProjects = () => {
setProjectCache(
@@ -348,6 +354,20 @@ function createGlobalSync() {
}
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()
})
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index 869f8b7eaa..cf104ad97f 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -43,8 +43,10 @@ function waitForPaint() {
const timer = setTimeout(finish, 50)
if (typeof requestAnimationFrame !== "function") return
requestAnimationFrame(() => {
- clearTimeout(timer)
- finish()
+ setTimeout(() => {
+ clearTimeout(timer)
+ finish()
+ }, 0)
})
})
}
@@ -87,12 +89,6 @@ export async function bootstrapGlobal(input: {
setGlobalStore: SetStoreFunction
}) {
const fast = [
- () =>
- retry(() =>
- input.globalSDK.path.get().then((x) => {
- input.setGlobalStore("path", x.data!)
- }),
- ),
() =>
retry(() =>
input.globalSDK.global.config.get().then((x) => {
@@ -108,6 +104,12 @@ export async function bootstrapGlobal(input: {
]
const slow = [
+ () =>
+ retry(() =>
+ input.globalSDK.path.get().then((x) => {
+ input.setGlobalStore("path", x.data!)
+ }),
+ ),
() =>
retry(() =>
input.globalSDK.project.list().then((x) => {
@@ -221,12 +223,16 @@ export async function bootstrapDirectory(input: {
if (loading) input.setStore("status", "partial")
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
? Promise.resolve()
: 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
? Promise.resolve()
@@ -237,7 +243,6 @@ export async function bootstrapDirectory(input: {
if (next) input.setStore("project", next)
}),
),
- () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
() =>
retry(() =>
input.sdk.vcs.get().then((x) => {
@@ -299,9 +304,6 @@ export async function bootstrapDirectory(input: {
)
}),
),
- ]
-
- const slow = [
() => Promise.resolve(input.loadSessions(input.directory)),
() =>
retry(() =>
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index 640d5e02eb..aafa4fb66c 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -544,12 +544,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
})
+ let sessionFrame: number | undefined
+ let sessionTimer: number | undefined
+
onMount(() => {
- Promise.all(
- server.projects.list().map((project) => {
- return globalSync.project.loadSessions(project.worktree)
- }),
- )
+ sessionFrame = requestAnimationFrame(() => {
+ sessionFrame = undefined
+ 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 {
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 1171ca9053..1204fba557 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -94,7 +94,11 @@ export namespace ServerConnection {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
- init: (props: { defaultServer: ServerConnection.Key; servers?: Array }) => {
+ init: (props: {
+ defaultServer: ServerConnection.Key
+ disableHealthCheck?: boolean
+ servers?: Array
+ }) => {
const checkServerHealth = useCheckServerHealth()
const [store, setStore, _, ready] = persisted(
@@ -202,6 +206,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const current_ = current()
if (!current_) return
+ if (props.disableHealthCheck) {
+ setState("healthy", true)
+ return
+ }
setState("healthy", undefined)
onCleanup(startHealthPolling(current_))
})
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index 4402855a6e..ae7768f71a 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -32,8 +32,8 @@ export interface Settings {
}
appearance: {
fontSize: number
- font: string
- uiFont: string
+ mono: string
+ sans: string
}
keybinds: Record
permissions: {
@@ -43,20 +43,18 @@ export interface Settings {
sounds: SoundSettings
}
-export const monoDefault = "IBM Plex Mono"
-export const sansDefault = "Inter"
+export const monoDefault = "System Mono"
+export const sansDefault = "System Sans"
const monoFallback =
'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 monoBase = `"${monoDefault}", "IBM Plex Mono Fallback", ${monoFallback}`
-const sansBase = `"${sansDefault}", "Inter Fallback", ${sansFallback}`
-const monoKey = "ibm-plex-mono"
+const monoBase = monoFallback
+const sansBase = sansFallback
-function input(font: string | undefined, key?: string) {
- if (!font || font === key || !font.trim()) return ""
- return font
+function input(font: string | undefined) {
+ return font ?? ""
}
function family(font: string) {
@@ -64,14 +62,14 @@ function family(font: string) {
return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
}
-function stack(font: string | undefined, base: string, key?: string) {
- const value = input(font, key).trim()
+function stack(font: string | undefined, base: string) {
+ const value = font?.trim() ?? ""
if (!value) return base
return `${family(value)}, ${base}`
}
export function monoInput(font: string | undefined) {
- return input(font, monoKey)
+ return input(font)
}
export function sansInput(font: string | undefined) {
@@ -79,7 +77,7 @@ export function sansInput(font: string | undefined) {
}
export function monoFontFamily(font: string | undefined) {
- return stack(font, monoBase, monoKey)
+ return stack(font, monoBase)
}
export function sansFontFamily(font: string | undefined) {
@@ -100,8 +98,8 @@ const defaultSettings: Settings = {
},
appearance: {
fontSize: 14,
- font: "",
- uiFont: "",
+ mono: "",
+ sans: "",
},
keybinds: {},
permissions: {
@@ -134,8 +132,8 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
createEffect(() => {
if (typeof document === "undefined") return
const root = document.documentElement
- root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
- root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.uiFont))
+ root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.mono))
+ root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans))
})
return {
@@ -189,13 +187,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setFontSize(value: number) {
setStore("appearance", "fontSize", value)
},
- font: withFallback(() => store.appearance?.font, defaultSettings.appearance.font),
+ font: withFallback(() => store.appearance?.mono, defaultSettings.appearance.mono),
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) {
- setStore("appearance", "uiFont", value.trim() ? value : "")
+ setStore("appearance", "sans", value.trim() ? value : "")
},
},
keybinds: {
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 11e6375b3b..917de35b1f 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -544,6 +544,8 @@ export default function Page() {
let reviewFrame: number | undefined
let refreshFrame: number | undefined
let refreshTimer: number | undefined
+ let todoFrame: number | undefined
+ let todoTimer: number | undefined
let diffFrame: number | undefined
let diffTimer: number | undefined
@@ -718,7 +720,6 @@ export default function Page() {
if (!info) return true
return Date.now() - info.at > SESSION_PREFETCH_TTL
})()
- const todos = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
untrack(() => {
void sync.session.sync(id)
})
@@ -730,13 +731,47 @@ export default function Page() {
if (params.id !== id) return
untrack(() => {
if (stale) void sync.session.sync(id, { force: true })
- void sync.session.todo(id, todos ? { force: true } : undefined)
})
}, 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(
on(
() => visibleUserMessages().at(-1)?.id,
@@ -1658,6 +1693,8 @@ export default function Page() {
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
+ if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
+ if (todoTimer !== undefined) window.clearTimeout(todoTimer)
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
diff --git a/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Bold.woff2 b/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Bold.woff2
deleted file mode 100644
index b441202d1c..0000000000
Binary files a/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Bold.woff2 and /dev/null differ
diff --git a/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Medium.woff2 b/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Medium.woff2
deleted file mode 100644
index d726b57c5c..0000000000
Binary files a/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Medium.woff2 and /dev/null differ
diff --git a/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Regular.woff2 b/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Regular.woff2
deleted file mode 100644
index 8c8a38b91b..0000000000
Binary files a/packages/ui/src/assets/fonts/BlexMonoNerdFontMono-Regular.woff2 and /dev/null differ
diff --git a/packages/ui/src/assets/fonts/ibm-plex-mono-bold.woff2 b/packages/ui/src/assets/fonts/ibm-plex-mono-bold.woff2
deleted file mode 120000
index f31cff001f..0000000000
--- a/packages/ui/src/assets/fonts/ibm-plex-mono-bold.woff2
+++ /dev/null
@@ -1 +0,0 @@
-BlexMonoNerdFontMono-Bold.woff2
\ No newline at end of file
diff --git a/packages/ui/src/assets/fonts/ibm-plex-mono-medium.woff2 b/packages/ui/src/assets/fonts/ibm-plex-mono-medium.woff2
deleted file mode 120000
index 50487e3c28..0000000000
--- a/packages/ui/src/assets/fonts/ibm-plex-mono-medium.woff2
+++ /dev/null
@@ -1 +0,0 @@
-BlexMonoNerdFontMono-Medium.woff2
\ No newline at end of file
diff --git a/packages/ui/src/assets/fonts/ibm-plex-mono.woff2 b/packages/ui/src/assets/fonts/ibm-plex-mono.woff2
deleted file mode 120000
index b47b298530..0000000000
--- a/packages/ui/src/assets/fonts/ibm-plex-mono.woff2
+++ /dev/null
@@ -1 +0,0 @@
-BlexMonoNerdFontMono-Regular.woff2
\ No newline at end of file
diff --git a/packages/ui/src/assets/fonts/inter.woff2 b/packages/ui/src/assets/fonts/inter.woff2
deleted file mode 100644
index b61bb0d0a5..0000000000
Binary files a/packages/ui/src/assets/fonts/inter.woff2 and /dev/null differ
diff --git a/packages/ui/src/components/font.stories.tsx b/packages/ui/src/components/font.stories.tsx
index 153a2c8dc9..f4e90bde09 100644
--- a/packages/ui/src/components/font.stories.tsx
+++ b/packages/ui/src/components/font.stories.tsx
@@ -2,24 +2,24 @@
import * as mod from "./font"
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
- No props.
### Variants and states
-- Fonts include sans and multiple mono families.
+- No variants.
### Behavior
-- Injects @font-face rules and preload links into the document head.
+- Compatibility wrapper only. No font assets are injected or preloaded.
### Accessibility
- Not applicable.
### Theming/tokens
-- Provides font families used by theme tokens.
+- Theme tokens come from CSS variables, not this component.
`
diff --git a/packages/ui/src/components/font.tsx b/packages/ui/src/components/font.tsx
index e1a508f16a..f89dfafe1d 100644
--- a/packages/ui/src/components/font.tsx
+++ b/packages/ui/src/components/font.tsx
@@ -1,63 +1 @@
-import { Link, Style } from "@solidjs/meta"
-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 (
- <>
-
-
-
-
-
- >
- )
-}
+export const Font = () => null
diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css
index 021f959e4c..751036598d 100644
--- a/packages/ui/src/styles/theme.css
+++ b/packages/ui/src/styles/theme.css
@@ -1,8 +1,9 @@
:root {
- --font-family-sans: "Inter", "Inter Fallback";
- --font-family-sans--font-feature-settings: "ss03" 1;
- --font-family-mono: "IBM Plex Mono", "IBM Plex Mono Fallback";
- --font-family-mono--font-feature-settings: "ss01" 1;
+ --font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-family-sans--font-feature-settings: normal;
+ --font-family-mono:
+ 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-base: 14px;