Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -445,6 +445,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 @@ -1012,6 +1012,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 @@ -103,6 +103,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})

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

sdk.event.listen((e) => {
const event = e.details
Expand Down Expand Up @@ -194,6 +195,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 @@ -206,6 +208,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 @@ -225,6 +228,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 @@ -266,6 +286,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 @@ -319,6 +340,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 @@ -431,7 +453,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 @@ -457,10 +461,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