Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
18cc1dc
feat: add collapse/float compaction modes and knowledge pack support
ryangamerdev Feb 21, 2026
0fcad9c
feat: add minFloat threshold to float compaction mode
ryangamerdev Feb 22, 2026
12d6635
fix: use delta token accounting in float sub-collapse minFloat gate
ryangamerdev Feb 22, 2026
7c1257b
fix: persist token adjustment to database after each float sub-collapse
ryangamerdev Feb 22, 2026
bd7c79c
feat: propagate parent session knowledge packs into subagent sessions
ryangamerdev Feb 23, 2026
0ce7172
feat: knowledge pack agent prompt overrides
ryangamerdev Feb 23, 2026
157fec9
fix: restore prompt focus after any sidebar mouse interaction
ryangamerdev Feb 23, 2026
89c2d2c
fix: scope prompt refocus to knowledge pack interactions only
ryangamerdev Feb 23, 2026
be69668
fix: detect full chain across mid-run user interjections in float/col…
ryangamerdev Mar 1, 2026
abc9f93
fix: inject knowledge pack messages into plugin transform hook and to…
ryangamerdev Mar 1, 2026
efbe9cb
fix: resolve sidebar click race condition causing TUI input lockup
ryangamerdev Mar 1, 2026
632ff80
fix: defer refetchActive after server responds to update KP enabled s…
ryangamerdev Mar 1, 2026
fbb4acb
fix: mirror global knowledge packs to project config on sidebar add/r…
Mar 2, 2026
83d3939
fix: write project config to .opencode/opencode.json not config.json
Mar 2, 2026
8fccbb4
fix: guard removeProjectKnowledgePack against unnecessary writes
Mar 2, 2026
fefe0e8
fix: run float pre-check on stop finish before exiting prompt loop
Mar 6, 2026
bfbd009
fix: use most recent non-zero-token assistant for float/overflow toke…
Mar 6, 2026
a38e15a
Revert "fix: use most recent non-zero-token assistant for float/overf…
Mar 6, 2026
f5a18a3
fix: only absorb user message as mid-run interjection when prev assis…
Mar 6, 2026
7abce5b
Updated
Mar 6, 2026
370101d
docs: document debug logging added to detectChains in bea7aa2dd
Mar 6, 2026
885c467
fix: port 1.3 fixes - filterCompacted error guard and overflow explan…
ryangamerdev Mar 15, 2026
4edb177
feat: add compaction model override for session compaction operations
ryangamerdev Mar 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,20 @@ function App() {
local.model.cycleFavorite(-1)
},
},
{
title: "Switch compaction model",
value: "compaction_model.list",
keybind: "compaction_model_list",
category: "Agent",
slash: {
name: "compaction-models",
aliases: ["compaction-model"],
},
onSelect: () => {
dialog.replace(() => <DialogModel target="compaction" />)
},
},

{
title: "Switch agent",
value: "agent.list",
Expand Down
53 changes: 40 additions & 13 deletions packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function useConnected() {
)
}

export function DialogModel(props: { providerID?: string }) {
export function DialogModel(props: { providerID?: string; target?: "session" | "compaction" }) {
const local = useLocal()
const sync = useSync()
const dialog = useDialog()
Expand All @@ -25,14 +25,41 @@ export function DialogModel(props: { providerID?: string }) {
const connected = useConnected()
const providers = createDialogProviderOptions()

const isCompaction = props.target === "compaction"

const showExtra = createMemo(() => connected() && !props.providerID)

function onModelSelect(model: { providerID: string; modelID: string }) {
dialog.clear()
if (isCompaction) {
local.model.compaction.set(model)
return
}
local.model.set(model, { recent: true })
}

const options = createMemo(() => {
const needle = query().trim()
const showSections = showExtra() && needle.length === 0
const favorites = connected() ? local.model.favorite() : []
const recents = local.model.recent()

// "Use session model (default)" option only shown in compaction mode
const defaultOption = isCompaction
? [
{
value: { providerID: "", modelID: "" },
title: "Use session model (default)",
description: "Compaction will use the same model as the session",
category: showSections ? "Default" : undefined,
onSelect: () => {
dialog.clear()
local.model.compaction.clear()
},
},
]
: []

function toOptions(items: typeof favorites, category: string) {
if (!showSections) return []
return items.flatMap((item) => {
Expand All @@ -49,10 +76,7 @@ export function DialogModel(props: { providerID?: string }) {
category,
disabled: provider.id === "opencode" && model.id.includes("-nano"),
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect: () => {
dialog.clear()
local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
},
onSelect: () => onModelSelect({ providerID: provider.id, modelID: model.id }),
},
]
})
Expand Down Expand Up @@ -87,10 +111,7 @@ export function DialogModel(props: { providerID?: string }) {
category: connected() ? provider.name : undefined,
disabled: provider.id === "opencode" && model.includes("-nano"),
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect() {
dialog.clear()
local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
},
onSelect: () => onModelSelect({ providerID: provider.id, modelID: model }),
})),
filter((x) => {
if (!showSections) return true
Expand Down Expand Up @@ -121,19 +142,22 @@ export function DialogModel(props: { providerID?: string }) {

if (needle) {
return [
...defaultOption,
...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
]
}

return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
return [...defaultOption, ...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
})

const provider = createMemo(() =>
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
)

const title = createMemo(() => provider()?.name ?? "Select model")
const title = createMemo(() => (isCompaction ? "Select compaction model" : (provider()?.name ?? "Select model")))

const current = createMemo(() => (isCompaction ? local.model.compaction.current() : local.model.current()))

return (
<DialogSelect<ReturnType<typeof options>[number]["value"]>
Expand All @@ -151,15 +175,18 @@ export function DialogModel(props: { providerID?: string }) {
title: "Favorite",
disabled: !connected(),
onTrigger: (option) => {
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
const val = option.value as { providerID: string; modelID: string }
if (val.providerID && val.modelID) {
local.model.toggleFavorite(val)
}
},
},
]}
onFilter={setQuery}
flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
current={current()}
/>
)
}
4 changes: 4 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,10 @@ export function Prompt(props: PromptProps) {
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
</text>
</Show>
<Show when={local.model.compaction.current()}>
<text fg={theme.textMuted}>·</text>
<text fg={theme.textMuted}>compact: {local.model.compaction.parsed().model}</text>
</Show>
</box>
</Show>
</box>
Expand Down
40 changes: 40 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
import { useKV } from "./kv"

export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
init: () => {
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const kv = useKV()

function isModelValid(model: { providerID: string; modelID: string }) {
const provider = sync.data.provider.find((x) => x.id === model.providerID)
Expand Down Expand Up @@ -320,6 +322,44 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
save()
})
},
compaction: iife(() => {
const key = "compaction_model"
const [get] = kv.signal<{ providerID: string; modelID: string } | undefined>(key, undefined)
return {
current() {
return get() as { providerID: string; modelID: string } | undefined
},
parsed: createMemo(() => {
const value = get() as { providerID: string; modelID: string } | undefined
if (!value) {
return {
provider: undefined,
model: "Using session model",
}
}
const provider = sync.data.provider.find((x) => x.id === value.providerID)
const info = provider?.models[value.modelID]
return {
provider: provider?.name ?? value.providerID,
model: info?.name ?? value.modelID,
}
}),
set(model: { providerID: string; modelID: string }) {
if (!isModelValid(model)) {
toast.show({
message: `Model ${model.providerID}/${model.modelID} is not valid`,
variant: "warning",
duration: 3000,
})
return
}
kv.set(key, { ...model })
},
clear() {
kv.set(key, undefined)
},
}
}),
variant: {
current() {
const m = currentModel()
Expand Down
23 changes: 22 additions & 1 deletion packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})

const sdk = useSDK()
const fullSyncedSessions = new Set<string>()

async function syncWorkspaces() {
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
Expand Down Expand Up @@ -203,6 +204,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break

case "session.deleted": {
if (!store.session) break
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore(
Expand All @@ -215,6 +217,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
case "session.updated": {
if (!store.session) break
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
Expand All @@ -234,6 +237,23 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}

case "session.compacted": {
// Compaction modified messages, invalidate cache and reload
const sessionID = event.properties.sessionID
fullSyncedSessions.delete(sessionID)
sdk.client.session.messages({ sessionID, limit: 100 }).then((messages) => {
setStore(
produce((draft) => {
draft.message[sessionID] = messages.data!.map((x) => x.info)
for (const message of messages.data!) {
draft.part[message.info.id] = message.parts
}
}),
)
})
break
}

case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
Expand Down Expand Up @@ -275,6 +295,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
case "message.removed": {
const messages = store.message[event.properties.sessionID]
if (!messages) break
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
setStore(
Expand Down Expand Up @@ -328,6 +349,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({

case "message.part.removed": {
const parts = store.part[event.properties.messageID]
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found)
setStore(
Expand Down Expand Up @@ -441,7 +463,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
bootstrap()
})

const fullSyncedSessions = new Set<string>()
const result = {
data: store,
set: setStore,
Expand Down
19 changes: 19 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ export function Session() {
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
const [compactionMethod, setCompactionMethod] = kv.signal<"standard" | "collapse" | "float">(
"compaction_method",
sync.data.config.compaction?.method ?? "standard",
)

const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
Expand Down Expand Up @@ -468,10 +472,25 @@ export function Session() {
})
return
}
const compactionModel = local.model.compaction.current()
sdk.client.session.summarize({
sessionID: route.sessionID,
modelID: selectedModel.modelID,
providerID: selectedModel.providerID,
compactionModel: compactionModel ?? undefined,
})
dialog.clear()
},
},
{
title: `Compaction: ${compactionMethod()} -> ${compactionMethod() === "standard" ? "collapse" : compactionMethod() === "collapse" ? "float" : "standard"}`,
value: "session.toggle.compaction_method",
category: "Session",
onSelect: (dialog) => {
setCompactionMethod((prev) => {
if (prev === "standard") return "collapse"
if (prev === "collapse") return "float"
return "standard"
})
dialog.clear()
},
Expand Down
Loading
Loading