Skip to content
Closed
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 @@ -442,6 +442,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
2 changes: 2 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 @@ -430,10 +430,12 @@ 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()
},
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,7 @@ export namespace Config {
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
compaction_model_list: z.string().optional().default("none").describe("List available compaction models"),
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ export const SessionRoutes = lazy(() =>
providerID: z.string(),
modelID: z.string(),
auto: z.boolean().optional().default(false),
compactionModel: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
}),
),
async (c) => {
Expand All @@ -535,6 +541,7 @@ export const SessionRoutes = lazy(() =>
modelID: body.modelID,
},
auto: body.auto,
compactionModel: body.compactionModel,
})
await SessionPrompt.loop({ sessionID })
return c.json(true)
Expand Down
16 changes: 13 additions & 3 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,15 @@ export namespace SessionCompaction {
sessionID: string
abort: AbortSignal
auto: boolean
compactionModel?: { providerID: string; modelID: string }
}) {
const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User
const agent = await Agent.get("compaction")
const model = agent.model
? await Provider.getModel(agent.model.providerID, agent.model.modelID)
: await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
const model = input.compactionModel
? await Provider.getModel(input.compactionModel.providerID, input.compactionModel.modelID)
: agent.model
? await Provider.getModel(agent.model.providerID, agent.model.modelID)
: await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
const msg = (await Session.updateMessage({
id: Identifier.ascending("message"),
role: "assistant",
Expand Down Expand Up @@ -237,6 +240,12 @@ When constructing the summary, try to stick to this template:
modelID: z.string(),
}),
auto: z.boolean(),
compactionModel: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
}),
async (input) => {
const msg = await Session.updateMessage({
Expand All @@ -255,6 +264,7 @@ When constructing the summary, try to stick to this template:
sessionID: msg.sessionID,
type: "compaction",
auto: input.auto,
compactionModel: input.compactionModel,
})
},
)
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ export namespace MessageV2 {
export const CompactionPart = PartBase.extend({
type: z.literal("compaction"),
auto: z.boolean(),
compactionModel: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
}).meta({
ref: "CompactionPart",
})
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ export namespace SessionPrompt {
abort,
sessionID,
auto: task.auto,
compactionModel: task.compactionModel,
})
if (result === "stop") break
continue
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,10 @@ export class Session2 extends HeyApiClient {
providerID?: string
modelID?: string
auto?: boolean
compactionModel?: {
providerID: string
modelID: string
}
},
options?: Options<never, ThrowOnError>,
) {
Expand All @@ -1455,6 +1459,7 @@ export class Session2 extends HeyApiClient {
{ in: "body", key: "providerID" },
{ in: "body", key: "modelID" },
{ in: "body", key: "auto" },
{ in: "body", key: "compactionModel" },
],
},
],
Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ export type CompactionPart = {
messageID: string
type: "compaction"
auto: boolean
compactionModel?: {
providerID: string
modelID: string
}
}

export type Part =
Expand Down Expand Up @@ -1167,6 +1171,10 @@ export type KeybindsConfig = {
* Previous favorite model
*/
model_cycle_favorite_reverse?: string
/**
* List available compaction models
*/
compaction_model_list?: string
/**
* List available commands
*/
Expand Down Expand Up @@ -3432,6 +3440,10 @@ export type SessionSummarizeData = {
providerID: string
modelID: string
auto?: boolean
compactionModel?: {
providerID: string
modelID: string
}
}
path: {
/**
Expand Down
Loading
Loading