+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
import { createSimpleContext } from "@opencode-ai/ui/context"
|
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
import { useParams } from "@solidjs/router"
|
|
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
|
import { createStore } from "solid-js/store"
|
|
import { useModels } from "@/context/models"
|
|
import { useProviders } from "@/hooks/use-providers"
|
|
import { Persist, persisted } from "@/utils/persist"
|
|
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
|
import { useSDK } from "./sdk"
|
|
import { useSync } from "./sync"
|
|
import { useServerSDK } from "./server-sdk"
|
|
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
|
|
|
export type ModelKey = { providerID: string; modelID: string; variant?: string }
|
|
|
|
type State = {
|
|
agent?: string
|
|
model?: ModelKey
|
|
variant?: string | null
|
|
}
|
|
|
|
type Saved = {
|
|
session: Record<string, State | undefined>
|
|
}
|
|
|
|
const WORKSPACE_KEY = "__workspace__"
|
|
const handoff = new Map<string, State>()
|
|
|
|
const handoffKey = (scope: ServerScope, dir: string, id: string) => ScopedKey.from(scope, dir, id)
|
|
|
|
const migrate = (value: unknown) => {
|
|
if (!value || typeof value !== "object") return { session: {} }
|
|
|
|
const item = value as {
|
|
session?: Record<string, State | undefined>
|
|
pick?: Record<string, State | undefined>
|
|
}
|
|
|
|
if (item.session && typeof item.session === "object") return { session: item.session }
|
|
if (!item.pick || typeof item.pick !== "object") return { session: {} }
|
|
|
|
return {
|
|
session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)),
|
|
}
|
|
}
|
|
|
|
const clone = (value: State | undefined) => {
|
|
if (!value) return
|
|
return {
|
|
...value,
|
|
model: value.model ? { ...value.model } : undefined,
|
|
} satisfies State
|
|
}
|
|
|
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
name: "Local",
|
|
init: () => {
|
|
const params = useParams()
|
|
const sdk = useSDK()
|
|
const sync = useSync()
|
|
const serverSDK = useServerSDK()
|
|
const providers = useProviders(() => sdk().directory)
|
|
const models = useModels()
|
|
|
|
const id = createMemo(() => params.id || undefined)
|
|
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
|
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
|
|
|
const [saved, setSaved] = persisted(
|
|
{
|
|
...Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"]),
|
|
migrate,
|
|
},
|
|
createStore<Saved>({
|
|
session: {},
|
|
}),
|
|
)
|
|
|
|
const [store, setStore] = createStore<{
|
|
current?: string
|
|
draft?: State
|
|
promoting?: State
|
|
last?: {
|
|
type: "agent" | "model" | "variant"
|
|
agent?: string
|
|
model?: ModelKey | null
|
|
variant?: string | null
|
|
}
|
|
}>({
|
|
current: list()[0]?.name,
|
|
draft: undefined,
|
|
last: undefined,
|
|
})
|
|
|
|
const validModel = (model: ModelKey) => {
|
|
const provider = providers.all().get(model.providerID)
|
|
return !!provider?.models[model.modelID] && connected().has(model.providerID)
|
|
}
|
|
|
|
const firstModel = (...items: Array<() => ModelKey | undefined>) => {
|
|
for (const item of items) {
|
|
const model = item()
|
|
if (!model) continue
|
|
if (validModel(model)) return model
|
|
}
|
|
}
|
|
|
|
const pickAgent = (name: string | undefined) => {
|
|
const items = list()
|
|
if (items.length === 0) return
|
|
return items.find((item) => item.name === name) ?? items[0]
|
|
}
|
|
|
|
createEffect(() => {
|
|
const items = list()
|
|
if (items.length === 0) {
|
|
if (store.current !== undefined) setStore("current", undefined)
|
|
return
|
|
}
|
|
if (items.some((item) => item.name === store.current)) return
|
|
setStore("current", items[0]?.name)
|
|
})
|
|
|
|
const scope = createMemo<State | undefined>(() => {
|
|
const session = id()
|
|
if (!session) return store.draft ?? store.promoting
|
|
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
|
|
})
|
|
|
|
createEffect(() => {
|
|
const session = id()
|
|
if (!session) return
|
|
|
|
const key = handoffKey(serverSDK().scope, sdk().directory, session)
|
|
const next = handoff.get(key)
|
|
if (!next) return
|
|
if (saved.session[session] !== undefined) {
|
|
handoff.delete(key)
|
|
setStore("promoting", undefined)
|
|
return
|
|
}
|
|
|
|
setSaved("session", session, clone(next))
|
|
handoff.delete(key)
|
|
setStore("promoting", undefined)
|
|
})
|
|
|
|
const configuredModel = () => {
|
|
const configured = sync().data.config.model
|
|
if (!configured) return
|
|
const [providerID, modelID] = configured.split("/")
|
|
const model = { providerID, modelID }
|
|
if (validModel(model)) return model
|
|
}
|
|
|
|
const recentModel = () => {
|
|
for (const item of models.recent.list()) {
|
|
if (validModel(item)) return item
|
|
}
|
|
}
|
|
|
|
const defaultModel = () => {
|
|
const defaults = providers.default()
|
|
for (const provider of providers.connected()) {
|
|
const configured = defaults[provider.id]
|
|
if (configured) {
|
|
const model = { providerID: provider.id, modelID: configured }
|
|
if (validModel(model)) return model
|
|
}
|
|
|
|
const first = Object.values(provider.models)[0]
|
|
if (!first) continue
|
|
const model = { providerID: provider.id, modelID: first.id }
|
|
if (validModel(model)) return model
|
|
}
|
|
}
|
|
|
|
const fallback = createMemo<ModelKey | undefined>(() => configuredModel() ?? recentModel() ?? defaultModel())
|
|
|
|
const agent = {
|
|
list,
|
|
current() {
|
|
return pickAgent(scope()?.agent ?? store.current)
|
|
},
|
|
set(name: string | undefined) {
|
|
const item = pickAgent(name)
|
|
if (!item) {
|
|
setStore("current", undefined)
|
|
return
|
|
}
|
|
|
|
batch(() => {
|
|
setStore("current", item.name)
|
|
setStore("last", {
|
|
type: "agent",
|
|
agent: item.name,
|
|
model: item.model,
|
|
variant: item.variant ?? null,
|
|
})
|
|
const prev = scope()
|
|
const next = {
|
|
agent: item.name,
|
|
model: item.model ?? prev?.model,
|
|
variant: item.variant ?? prev?.variant,
|
|
} satisfies State
|
|
const session = id()
|
|
if (session) {
|
|
setSaved("session", session, next)
|
|
return
|
|
}
|
|
setStore("draft", next)
|
|
})
|
|
},
|
|
move(direction: 1 | -1) {
|
|
const items = list()
|
|
if (items.length === 0) {
|
|
setStore("current", undefined)
|
|
return
|
|
}
|
|
|
|
let next = items.findIndex((item) => item.name === agent.current()?.name) + direction
|
|
if (next < 0) next = items.length - 1
|
|
if (next >= items.length) next = 0
|
|
const item = items[next]
|
|
if (!item) return
|
|
agent.set(item.name)
|
|
},
|
|
}
|
|
|
|
const current = () => {
|
|
const item = firstModel(
|
|
() => scope()?.model,
|
|
() => agent.current()?.model,
|
|
fallback,
|
|
)
|
|
if (!item) return
|
|
return models.find(item)
|
|
}
|
|
|
|
const configured = () => {
|
|
const item = agent.current()
|
|
const model = current()
|
|
if (!item || !model) return
|
|
return getConfiguredAgentVariant({
|
|
agent: { model: item.model, variant: item.variant },
|
|
model: { providerID: model.provider.id, modelID: model.id, variants: model.variants },
|
|
})
|
|
}
|
|
|
|
const selected = () => scope()?.variant
|
|
|
|
const snapshot = () => {
|
|
const model = current()
|
|
return {
|
|
agent: agent.current()?.name,
|
|
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
|
variant: selected(),
|
|
} satisfies State
|
|
}
|
|
|
|
const write = (next: Partial<State>) => {
|
|
const state = {
|
|
...(scope() ?? { agent: agent.current()?.name }),
|
|
...next,
|
|
} satisfies State
|
|
|
|
const session = id()
|
|
if (session) {
|
|
setSaved("session", session, state)
|
|
return
|
|
}
|
|
setStore("draft", state)
|
|
}
|
|
|
|
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
|
|
|
const model = {
|
|
ready: models.ready,
|
|
current,
|
|
recent,
|
|
list: models.list,
|
|
cycle(direction: 1 | -1) {
|
|
const items = recent()
|
|
const item = current()
|
|
if (!item) return
|
|
|
|
const index = items.findIndex((entry) => entry?.provider.id === item.provider.id && entry?.id === item.id)
|
|
if (index === -1) return
|
|
|
|
let next = index + direction
|
|
if (next < 0) next = items.length - 1
|
|
if (next >= items.length) next = 0
|
|
|
|
const entry = items[next]
|
|
if (!entry) return
|
|
model.set({ providerID: entry.provider.id, modelID: entry.id })
|
|
},
|
|
set(item: ModelKey | undefined, options?: { recent?: boolean }) {
|
|
startTransition(() =>
|
|
batch(() => {
|
|
setStore("last", {
|
|
type: "model",
|
|
agent: agent.current()?.name,
|
|
model: item ?? null,
|
|
variant: selected(),
|
|
})
|
|
write({ model: item })
|
|
if (!item) return
|
|
models.setVisibility(item, true)
|
|
if (!options?.recent) return
|
|
models.recent.push(item)
|
|
}),
|
|
)
|
|
},
|
|
visible(item: ModelKey) {
|
|
return models.visible(item)
|
|
},
|
|
setVisibility(item: ModelKey, visible: boolean) {
|
|
models.setVisibility(item, visible)
|
|
},
|
|
variant: {
|
|
configured,
|
|
selected,
|
|
current() {
|
|
const resolved = resolveModelVariant({
|
|
variants: this.list(),
|
|
selected: this.selected(),
|
|
configured: this.configured(),
|
|
})
|
|
if (resolved) return resolved
|
|
const model = current()
|
|
if (!model) return
|
|
const saved = models.variant.get({ providerID: model.provider.id, modelID: model.id })
|
|
if (saved && this.list().includes(saved)) return saved
|
|
},
|
|
list() {
|
|
const item = current()
|
|
if (!item?.variants) return []
|
|
return Object.keys(item.variants)
|
|
},
|
|
set(value: string | undefined) {
|
|
startTransition(() =>
|
|
batch(() => {
|
|
const model = current()
|
|
setStore("last", {
|
|
type: "variant",
|
|
agent: agent.current()?.name,
|
|
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
|
|
variant: value ?? null,
|
|
})
|
|
write({ variant: value ?? null })
|
|
if (model) {
|
|
models.variant.set({ providerID: model.provider.id, modelID: model.id }, value ?? undefined)
|
|
}
|
|
}),
|
|
)
|
|
},
|
|
cycle() {
|
|
const items = this.list()
|
|
if (items.length === 0) return
|
|
this.set(
|
|
cycleModelVariant({
|
|
variants: items,
|
|
selected: this.selected(),
|
|
configured: this.configured(),
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
}
|
|
|
|
const result = {
|
|
slug: createMemo(() => base64Encode(sdk().directory)),
|
|
model,
|
|
agent,
|
|
session: {
|
|
reset() {
|
|
setStore({ draft: undefined, promoting: undefined })
|
|
},
|
|
promote(dir: string, session: string) {
|
|
const next = clone(snapshot())
|
|
if (!next) return
|
|
const key = handoffKey(serverSDK().scope, dir, session)
|
|
handoff.set(key, next)
|
|
|
|
if (dir === sdk().directory) {
|
|
setSaved("session", session, next)
|
|
}
|
|
|
|
setStore("promoting", next)
|
|
setStore("draft", undefined)
|
|
},
|
|
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
|
const session = id()
|
|
if (!session) return
|
|
if (msg.sessionID !== session) return
|
|
if (saved.session[session] !== undefined) return
|
|
if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return
|
|
|
|
setSaved("session", session, {
|
|
agent: msg.agent,
|
|
model: msg.model,
|
|
variant: msg.model?.variant ?? null,
|
|
})
|
|
},
|
|
},
|
|
}
|
|
return result
|
|
},
|
|
})
|