|
|
@@ -1,7 +1,7 @@
|
|
|
import { createStore } from "solid-js/store"
|
|
|
import { dedupeWith } from "effect/Array"
|
|
|
import { createSimpleContext } from "./helper"
|
|
|
-import { batch, createMemo } from "solid-js"
|
|
|
+import { batch, createMemo, onCleanup } from "solid-js"
|
|
|
import { useEvent } from "./event"
|
|
|
import path from "path"
|
|
|
import { useTuiPaths } from "./runtime"
|
|
|
@@ -22,6 +22,7 @@ import { useToast } from "../ui/toast"
|
|
|
import { useRoute } from "./route"
|
|
|
import { useData } from "./data"
|
|
|
import { usePermission } from "./permission"
|
|
|
+import { useLocation } from "./location"
|
|
|
|
|
|
export function parseModel(model: string) {
|
|
|
const [providerID, ...rest] = model.split("/")
|
|
|
@@ -57,26 +58,29 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
const args = useArgs()
|
|
|
const event = useEvent()
|
|
|
const permission = usePermission()
|
|
|
+ const location = useLocation()
|
|
|
+
|
|
|
+ const models = () => data.location.model.list(location.ref)
|
|
|
+ const providers = () => data.location.provider.list(location.ref)
|
|
|
|
|
|
function isModelValid(model: ModelPreferenceModel) {
|
|
|
- return !!data.location.model
|
|
|
- .list()
|
|
|
- ?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
|
|
|
+ return !!models()?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
|
|
|
}
|
|
|
|
|
|
function getFirstValidModel(...modelFns: (() => ModelPreferenceModel | undefined)[]) {
|
|
|
for (const modelFn of modelFns) {
|
|
|
const model = modelFn()
|
|
|
- if (!model) continue
|
|
|
- if (isModelValid(model)) return model
|
|
|
+ if (model && isModelValid(model)) return model
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function createAgent() {
|
|
|
const agents = createMemo(() =>
|
|
|
- (data.location.agent.list() ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
|
|
|
+ (data.location.agent.list(location.ref) ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
|
|
|
+ )
|
|
|
+ const visibleAgents = createMemo(() =>
|
|
|
+ (data.location.agent.list(location.ref) ?? []).filter((agent) => !agent.hidden),
|
|
|
)
|
|
|
- const visibleAgents = createMemo(() => (data.location.agent.list() ?? []).filter((agent) => !agent.hidden))
|
|
|
const [agentStore, setAgentStore] = createStore({
|
|
|
current: undefined as string | undefined,
|
|
|
})
|
|
|
@@ -128,35 +132,40 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
const agent = createAgent()
|
|
|
|
|
|
function createModel() {
|
|
|
- const [modelStore, setModelStore] = createStore<
|
|
|
- ModelPreference & {
|
|
|
- ready: boolean
|
|
|
- model: Record<string, ModelPreferenceModel>
|
|
|
- }
|
|
|
- >({
|
|
|
+ type ModelSelection = ModelPreferenceModel & { variant?: string }
|
|
|
+ const [preferences, setPreferences] = createStore<ModelPreference & { ready: boolean }>({
|
|
|
ready: false,
|
|
|
- model: {},
|
|
|
recent: [],
|
|
|
favorite: [],
|
|
|
variant: {},
|
|
|
})
|
|
|
+ const [selectionState, setSelectionState] = createStore<{
|
|
|
+ newSessionModelByLocationAgent: Record<string, ModelPreferenceModel | undefined>
|
|
|
+ draftBySession: Record<string, ModelSelection | undefined>
|
|
|
+ }>({
|
|
|
+ newSessionModelByLocationAgent: {},
|
|
|
+ draftBySession: {},
|
|
|
+ })
|
|
|
|
|
|
const repository = createModelPreferenceRepository(path.join(paths.state, "model.json"))
|
|
|
- const state = {
|
|
|
+ const pendingSelectionCommits = new Map<string, string>()
|
|
|
+ const selectionKey = (value: ModelSelection) =>
|
|
|
+ `${modelPreferenceKey(value)}:${normalizeModelVariant(value.variant) ?? "default"}`
|
|
|
+ const saveState = {
|
|
|
pending: false,
|
|
|
}
|
|
|
|
|
|
- function save() {
|
|
|
- if (!modelStore.ready) {
|
|
|
- state.pending = true
|
|
|
+ function savePreferences() {
|
|
|
+ if (!preferences.ready) {
|
|
|
+ saveState.pending = true
|
|
|
return
|
|
|
}
|
|
|
- state.pending = false
|
|
|
+ saveState.pending = false
|
|
|
void repository
|
|
|
.patch({
|
|
|
- recent: modelStore.recent,
|
|
|
- favorite: modelStore.favorite,
|
|
|
- variant: modelStore.variant,
|
|
|
+ recent: preferences.recent,
|
|
|
+ favorite: preferences.favorite,
|
|
|
+ variant: preferences.variant,
|
|
|
})
|
|
|
.catch(() => undefined)
|
|
|
}
|
|
|
@@ -164,14 +173,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
repository
|
|
|
.load()
|
|
|
.then((value) => {
|
|
|
- setModelStore("recent", value.recent)
|
|
|
- setModelStore("favorite", value.favorite)
|
|
|
- setModelStore("variant", value.variant)
|
|
|
+ setPreferences("recent", value.recent)
|
|
|
+ setPreferences("favorite", value.favorite)
|
|
|
+ setPreferences("variant", value.variant)
|
|
|
})
|
|
|
.catch(() => {})
|
|
|
.finally(() => {
|
|
|
- setModelStore("ready", true)
|
|
|
- if (state.pending) save()
|
|
|
+ setPreferences("ready", true)
|
|
|
+ if (saveState.pending) savePreferences()
|
|
|
})
|
|
|
|
|
|
const fallbackModel = createMemo(() => {
|
|
|
@@ -185,13 +194,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- for (const item of modelStore.recent) {
|
|
|
+ for (const item of preferences.recent) {
|
|
|
if (isModelValid(item)) {
|
|
|
return item
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const model = data.location.model.list()?.[0]
|
|
|
+ const model = models()?.[0]
|
|
|
if (!model) return undefined
|
|
|
return {
|
|
|
providerID: model.providerID,
|
|
|
@@ -199,30 +208,134 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- const currentModel = createMemo(() => {
|
|
|
+ const newSessionModel = createMemo(() => {
|
|
|
const a = agent.current()
|
|
|
- return (
|
|
|
- getFirstValidModel(
|
|
|
- () => a && modelStore.model[a.id],
|
|
|
- () => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
|
|
|
- fallbackModel,
|
|
|
- ) ?? undefined
|
|
|
+ return getFirstValidModel(
|
|
|
+ () => a && selectionState.newSessionModelByLocationAgent[locationAgentKey(a.id)],
|
|
|
+ () => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
|
|
|
+ fallbackModel,
|
|
|
)
|
|
|
})
|
|
|
|
|
|
+ const currentSelection = createMemo<ModelSelection | undefined>(() => {
|
|
|
+ if (route.data.type === "session") return sessionSelection(route.data.sessionID)
|
|
|
+ const model = newSessionModel()
|
|
|
+ if (!model) return
|
|
|
+ return { ...model, variant: normalizeModelVariant(preferences.variant[modelPreferenceKey(model)]) }
|
|
|
+ })
|
|
|
+
|
|
|
+ const currentModel = createMemo(() => {
|
|
|
+ const selection = currentSelection()
|
|
|
+ if (!selection) return
|
|
|
+ return { providerID: selection.providerID, modelID: selection.modelID }
|
|
|
+ })
|
|
|
+
|
|
|
+ function locationAgentKey(agentID: string) {
|
|
|
+ const ref = location.ref ?? data.location.default()
|
|
|
+ return `${JSON.stringify([ref.directory, ref.workspaceID])}:${agentID}`
|
|
|
+ }
|
|
|
+
|
|
|
+ function durableSelection(sessionID: string): ModelSelection | undefined {
|
|
|
+ const model = data.session.get(sessionID)?.model
|
|
|
+ if (!model) return
|
|
|
+ return {
|
|
|
+ providerID: model.providerID,
|
|
|
+ modelID: model.id,
|
|
|
+ variant: normalizeModelVariant(model.variant),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function sessionSelection(sessionID: string) {
|
|
|
+ return selectionState.draftBySession[sessionID] ?? durableSelection(sessionID)
|
|
|
+ }
|
|
|
+
|
|
|
+ function setSessionDraft(sessionID: string, selection: ModelSelection) {
|
|
|
+ const durable = durableSelection(sessionID)
|
|
|
+ setSelectionState(
|
|
|
+ "draftBySession",
|
|
|
+ sessionID,
|
|
|
+ durable && selectionKey(durable) === selectionKey(selection) ? undefined : selection,
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ function selectModel(model: ModelPreferenceModel) {
|
|
|
+ if (route.data.type === "session") {
|
|
|
+ const sessionID = route.data.sessionID
|
|
|
+ const current = sessionSelection(sessionID)
|
|
|
+ const preferred = normalizeModelVariant(
|
|
|
+ current?.providerID === model.providerID && current.modelID === model.modelID
|
|
|
+ ? current.variant
|
|
|
+ : preferences.variant[modelPreferenceKey(model)],
|
|
|
+ )
|
|
|
+ const info = models()?.find((item) => item.providerID === model.providerID && item.id === model.modelID)
|
|
|
+ const variant = preferred && info?.variants?.some((item) => item.id === preferred) ? preferred : undefined
|
|
|
+ setSessionDraft(sessionID, { ...model, variant })
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ const current = agent.current()
|
|
|
+ if (!current) return false
|
|
|
+ setSelectionState("newSessionModelByLocationAgent", locationAgentKey(current.id), model)
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ onCleanup(
|
|
|
+ event.on("session.model.selected", (evt) => {
|
|
|
+ const expected = pendingSelectionCommits.get(evt.data.sessionID)
|
|
|
+ if (!expected) return
|
|
|
+ const committed = selectionKey({
|
|
|
+ providerID: evt.data.model.providerID,
|
|
|
+ modelID: evt.data.model.id,
|
|
|
+ variant: evt.data.model.variant,
|
|
|
+ })
|
|
|
+ if (committed !== expected) return
|
|
|
+ pendingSelectionCommits.delete(evt.data.sessionID)
|
|
|
+ const draft = selectionState.draftBySession[evt.data.sessionID]
|
|
|
+ if (draft && selectionKey(draft) === committed)
|
|
|
+ setSelectionState("draftBySession", evt.data.sessionID, undefined)
|
|
|
+ }),
|
|
|
+ )
|
|
|
+
|
|
|
+ onCleanup(
|
|
|
+ event.on("session.deleted", (evt) => {
|
|
|
+ pendingSelectionCommits.delete(evt.data.sessionID)
|
|
|
+ setSelectionState("draftBySession", evt.data.sessionID, undefined)
|
|
|
+ }),
|
|
|
+ )
|
|
|
+
|
|
|
return {
|
|
|
current: currentModel,
|
|
|
+ selection: currentSelection,
|
|
|
+ available(model = currentModel()) {
|
|
|
+ return model ? isModelValid(model) : false
|
|
|
+ },
|
|
|
+ trackSessionCommit(
|
|
|
+ sessionID: string,
|
|
|
+ value: {
|
|
|
+ providerID: string
|
|
|
+ id: string
|
|
|
+ variant?: string
|
|
|
+ },
|
|
|
+ ) {
|
|
|
+ const committed = selectionKey({ providerID: value.providerID, modelID: value.id, variant: value.variant })
|
|
|
+ pendingSelectionCommits.set(sessionID, committed)
|
|
|
+ return () => {
|
|
|
+ if (pendingSelectionCommits.get(sessionID) === committed) pendingSelectionCommits.delete(sessionID)
|
|
|
+ }
|
|
|
+ },
|
|
|
get ready() {
|
|
|
- return modelStore.ready
|
|
|
+ return preferences.ready
|
|
|
+ },
|
|
|
+ get catalogReady() {
|
|
|
+ return models() !== undefined
|
|
|
},
|
|
|
recent() {
|
|
|
- return modelStore.recent
|
|
|
+ return preferences.recent
|
|
|
},
|
|
|
favorite() {
|
|
|
- return modelStore.favorite
|
|
|
+ return preferences.favorite
|
|
|
},
|
|
|
parsed: createMemo(() => {
|
|
|
- const value = currentModel()
|
|
|
+ const value = currentSelection()
|
|
|
if (!value) {
|
|
|
return {
|
|
|
provider: "Connect a provider",
|
|
|
@@ -230,33 +343,28 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
reasoning: false,
|
|
|
}
|
|
|
}
|
|
|
- const provider = data.location.provider.list()?.find((item) => item.id === value.providerID)
|
|
|
- const info = data.location.model
|
|
|
- .list()
|
|
|
- ?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
|
|
|
+ const provider = providers()?.find((item) => item.id === value.providerID)
|
|
|
+ const info = models()?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
|
|
|
return {
|
|
|
provider: provider?.name ?? value.providerID,
|
|
|
- model: info?.name ?? value.modelID,
|
|
|
+ model: info?.name ?? `${value.modelID} (unavailable)`,
|
|
|
reasoning: (info?.variants?.length ?? 0) !== 0,
|
|
|
}
|
|
|
}),
|
|
|
cycle(direction: 1 | -1) {
|
|
|
- const current = currentModel()
|
|
|
+ const current = currentSelection()
|
|
|
if (!current) return
|
|
|
- const recent = modelStore.recent
|
|
|
+ const recent = recentModels(current, preferences.recent).filter(isModelValid)
|
|
|
const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
|
|
- if (index === -1) return
|
|
|
- let next = index + direction
|
|
|
+ let next = index === -1 ? (direction === 1 ? 0 : recent.length - 1) : index + direction
|
|
|
if (next < 0) next = recent.length - 1
|
|
|
if (next >= recent.length) next = 0
|
|
|
const val = recent[next]
|
|
|
if (!val) return
|
|
|
- const a = agent.current()
|
|
|
- if (!a) return
|
|
|
- setModelStore("model", a.id, { ...val })
|
|
|
+ selectModel({ ...val })
|
|
|
},
|
|
|
cycleFavorite(direction: 1 | -1) {
|
|
|
- const favorites = modelStore.favorite.filter((item) => isModelValid(item))
|
|
|
+ const favorites = preferences.favorite.filter((item) => isModelValid(item))
|
|
|
if (!favorites.length) {
|
|
|
toast.show({
|
|
|
variant: "info",
|
|
|
@@ -265,7 +373,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
})
|
|
|
return
|
|
|
}
|
|
|
- const current = currentModel()
|
|
|
+ const current = currentSelection()
|
|
|
let index = -1
|
|
|
if (current) {
|
|
|
index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
|
|
@@ -279,45 +387,39 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
}
|
|
|
const next = favorites[index]
|
|
|
if (!next) return
|
|
|
- const a = agent.current()
|
|
|
- if (!a) return
|
|
|
- setModelStore("model", a.id, { ...next })
|
|
|
- setModelStore("recent", recentModels(next, modelStore.recent))
|
|
|
- save()
|
|
|
+ if (!selectModel({ ...next })) return
|
|
|
+ setPreferences("recent", recentModels(next, preferences.recent))
|
|
|
+ savePreferences()
|
|
|
},
|
|
|
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
|
|
batch(() => {
|
|
|
if (!isModelValid(model)) return
|
|
|
- const a = agent.current()
|
|
|
- if (!a) return
|
|
|
- setModelStore("model", a.id, model)
|
|
|
+ if (!selectModel(model)) return
|
|
|
if (options?.recent) {
|
|
|
- setModelStore("recent", recentModels(model, modelStore.recent))
|
|
|
- save()
|
|
|
+ setPreferences("recent", recentModels(model, preferences.recent))
|
|
|
+ savePreferences()
|
|
|
}
|
|
|
})
|
|
|
},
|
|
|
toggleFavorite(model: { providerID: string; modelID: string }) {
|
|
|
batch(() => {
|
|
|
if (!isModelValid(model)) return
|
|
|
- const exists = modelStore.favorite.some(
|
|
|
+ const exists = preferences.favorite.some(
|
|
|
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
|
|
|
)
|
|
|
const next = exists
|
|
|
- ? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
|
|
- : [model, ...modelStore.favorite]
|
|
|
- setModelStore(
|
|
|
+ ? preferences.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
|
|
+ : [model, ...preferences.favorite]
|
|
|
+ setPreferences(
|
|
|
"favorite",
|
|
|
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
|
|
)
|
|
|
- save()
|
|
|
+ savePreferences()
|
|
|
})
|
|
|
},
|
|
|
variant: {
|
|
|
selected() {
|
|
|
- const m = currentModel()
|
|
|
- if (!m) return undefined
|
|
|
- return normalizeModelVariant(modelStore.variant[modelPreferenceKey(m)])
|
|
|
+ return currentSelection()?.variant
|
|
|
},
|
|
|
current() {
|
|
|
const v = this.selected()
|
|
|
@@ -325,18 +427,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
return undefined
|
|
|
},
|
|
|
list() {
|
|
|
- const m = currentModel()
|
|
|
+ const m = currentSelection()
|
|
|
if (!m) return []
|
|
|
- const info = data.location.model
|
|
|
- .list()
|
|
|
- ?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
|
|
|
+ const info = models()?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
|
|
|
return info?.variants?.map((variant) => variant.id) ?? []
|
|
|
},
|
|
|
set(value: string | undefined) {
|
|
|
- const m = currentModel()
|
|
|
+ const m = currentSelection()
|
|
|
if (!m) return
|
|
|
- setModelStore("variant", modelPreferenceKey(m), normalizeModelVariant(value))
|
|
|
- save()
|
|
|
+ if (route.data.type === "session") {
|
|
|
+ setSessionDraft(route.data.sessionID, { ...m, variant: normalizeModelVariant(value) })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ setPreferences("variant", modelPreferenceKey(m), normalizeModelVariant(value))
|
|
|
+ savePreferences()
|
|
|
},
|
|
|
cycle() {
|
|
|
const variants = this.list()
|