diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index 1ea97c395c43..e1efeac71abe 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -1,17 +1,20 @@
-import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
+import { createMemo, createEffect, createSignal, on, onCleanup, For, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useParams } from "@solidjs/router"
import { useSync } from "@/context/sync"
+import { useSDK } from "@/context/sdk"
import { useLayout } from "@/context/layout"
import { checksum } from "@opencode-ai/util/encode"
import { findLast } from "@opencode-ai/util/array"
import { same } from "@/utils/same"
import { Icon } from "@opencode-ai/ui/icon"
+import { Button } from "@opencode-ai/ui/button"
import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Code } from "@opencode-ai/ui/code"
import { Markdown } from "@opencode-ai/ui/markdown"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
+import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language"
import { getSessionContextMetrics } from "./session-context-metrics"
@@ -278,6 +281,66 @@ export function SessionContextTab() {
onScroll={handleScroll}
>
+
+ {(c) => {
+ const sdk = useSDK()
+ const [compacting, setCompacting] = createSignal(false)
+ const usage = () => c().usage ?? 0
+ const color = () => usage() > 80 ? "var(--syntax-error)" : usage() > 60 ? "var(--syntax-warning)" : "var(--syntax-success)"
+
+ const compact = async () => {
+ if (!params.id || compacting()) return
+ setCompacting(true)
+ try {
+ const last = visibleUserMessages().at(-1)
+ await sdk.client.session.summarize({
+ sessionID: params.id,
+ directory: sdk.directory,
+ providerID: last?.model?.providerID,
+ modelID: last?.model?.modelID,
+ })
+ } finally {
+ setCompacting(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ {formatter().number(c().total)} / {formatter().number(c().limit)}
+
+
+ {usage()}% {language.t("context.usage.usage")}
+
+
+
+
+
+
+
+ )
+ }}
+
+
{(stat) => [0])} value={stat.value()} />}
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index 42ee4092f68c..2c620b95ef51 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -267,6 +267,48 @@ export const SettingsGeneral: Component = () => {
)}
+
+
+
+
+
+ {settings.appearance.fontSize()}
+
+
+
+
+
+
+
+ settings.appearance.setWideMode(checked)}
+ />
+
+
)
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index ce811463fc67..732bb169316d 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -337,7 +337,7 @@ export const Terminal = (props: TerminalProps) => {
cursorStyle: "bar",
cols: restoreSize?.cols,
rows: restoreSize?.rows,
- fontSize: 14,
+ fontSize: settings.appearance.fontSize(),
fontFamily: monoFontFamily(settings.appearance.font()),
allowTransparency: false,
convertEol: false,
diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx
index 8c0035d555b7..9fd7e898acf9 100644
--- a/packages/app/src/context/global-sdk.tsx
+++ b/packages/app/src/context/global-sdk.tsx
@@ -49,6 +49,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
let queue: Queued[] = []
let buffer: Queued[] = []
const coalesced = new Map()
+ const voided = new Set()
let timer: ReturnType | undefined
let last = 0
@@ -61,6 +62,19 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
}
}
+ const voidStaleDeltasForPart = (messageID: string, partID: string) => {
+ for (let i = 0; i < queue.length; i++) {
+ const entry = queue[i]
+ if (
+ entry.payload.type === "message.part.delta" &&
+ (entry.payload.properties as { messageID: string; partID: string }).messageID === messageID &&
+ (entry.payload.properties as { messageID: string; partID: string }).partID === partID
+ ) {
+ voided.add(i)
+ }
+ }
+ }
+
const flush = () => {
if (timer) clearTimeout(timer)
timer = undefined
@@ -75,11 +89,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
last = Date.now()
batch(() => {
- for (const event of events) {
- emitter.emit(event.directory, event.payload)
+ for (let i = 0; i < events.length; i++) {
+ if (voided.has(i)) continue
+ emitter.emit(events[i].directory, events[i].payload)
}
})
+ voided.clear()
buffer.length = 0
}
@@ -144,6 +160,10 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = { directory, payload }
+ if (payload.type === "message.part.updated") {
+ const part = (payload.properties as { part: { messageID: string; id: string } }).part
+ voidStaleDeltasForPart(part.messageID, part.id)
+ }
continue
}
coalesced.set(k, queue.length)
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index b43469b5c37c..5e4b42fd9f0d 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -32,6 +32,7 @@ export interface Settings {
appearance: {
fontSize: number
font: string
+ wideMode: boolean
}
keybinds: Record
permissions: {
@@ -55,6 +56,7 @@ const defaultSettings: Settings = {
appearance: {
fontSize: 14,
font: "ibm-plex-mono",
+ wideMode: false,
},
keybinds: {},
permissions: {
@@ -112,6 +114,14 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
})
+ createEffect(() => {
+ if (typeof document === "undefined") return
+ const size = store.appearance?.fontSize ?? defaultSettings.appearance.fontSize
+ document.documentElement.style.setProperty("--font-size-base", `${size}px`)
+ document.documentElement.style.setProperty("--font-size-small", `${size - 1}px`)
+ document.documentElement.style.setProperty("--font-size-large", `${size + 2}px`)
+ })
+
return {
ready,
get current() {
@@ -163,6 +173,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setFont(value: string) {
setStore("appearance", "font", value)
},
+ wideMode: withFallback(() => store.appearance?.wideMode, defaultSettings.appearance.wideMode),
+ setWideMode(value: boolean) {
+ setStore("appearance", "wideMode", value)
+ },
},
keybinds: {
get: (action: string) => store.keybinds?.[action],
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index e0ef92682d94..46e6a9e3922a 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -10,6 +10,7 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { Mark } from "@opencode-ai/ui/logo"
import { useSync } from "@/context/sync"
+import { useSettings } from "@/context/settings"
import { useLayout } from "@/context/layout"
import { checksum, base64Encode } from "@opencode-ai/util/encode"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -107,7 +108,8 @@ export default function Page() {
if (desktopReviewOpen()) return `${layout.session.width()}px`
return `calc(100% - ${layout.fileTree.width()}px)`
})
- const centered = createMemo(() => isDesktop() && !desktopSidePanelOpen())
+ const settings = useSettings()
+ const centered = createMemo(() => isDesktop() && !desktopSidePanelOpen() && !settings.appearance.wideMode())
function normalizeTab(tab: string) {
if (!tab.startsWith("file://")) return tab
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index e7532d20073b..736d6b75fdf6 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -354,7 +354,11 @@ export namespace SessionProcessor {
})
const error = MessageV2.fromError(e, { providerID: input.model.providerID })
if (MessageV2.ContextOverflowError.isInstance(error)) {
- // TODO: Handle context overflow error
+ log.info("context overflow detected, triggering compaction", {
+ sessionID: input.sessionID,
+ })
+ needsCompaction = true
+ break
}
const retry = SessionRetry.retryable(error)
if (retry !== undefined) {
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 75bd3c9dfaca..c8b428acdece 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -538,12 +538,13 @@ export namespace SessionPrompt {
continue
}
- // context overflow, needs compaction
+ // proactive context management: prune + compact before overflow
if (
lastFinished &&
lastFinished.summary !== true &&
(await SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model }))
) {
+ await SessionCompaction.prune({ sessionID })
await SessionCompaction.create({
sessionID,
agent: lastUser.agent,
@@ -702,6 +703,7 @@ export namespace SessionPrompt {
if (result === "stop") break
if (result === "compact") {
+ await SessionCompaction.prune({ sessionID })
await SessionCompaction.create({
sessionID,
agent: lastUser.agent,