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,