mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 17:19:49 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb5cd98dc7 | |||
| f471e51b95 | |||
| e5436ab5c1 | |||
| d4216c5d9a | |||
| c71b5d73d8 |
@@ -8,17 +8,21 @@ import * as fuzzysort from "fuzzysort"
|
||||
import { useConnected } from "./use-connected"
|
||||
import { useData } from "../context/data"
|
||||
import { modelPreferenceKey } from "../model-preference"
|
||||
import { useLocation } from "../context/location"
|
||||
|
||||
export function DialogModel(props: { providerID?: string }) {
|
||||
const local = useLocal()
|
||||
const data = useData()
|
||||
const dialog = useDialog()
|
||||
const location = useLocation()
|
||||
const [query, setQuery] = createSignal("")
|
||||
const favoritePriority = new Set(local.model.favorite().map(modelPreferenceKey))
|
||||
|
||||
const connected = useConnected()
|
||||
const providers = createMemo(() => new Map((data.location.provider.list() ?? []).map((item) => [item.id, item])))
|
||||
const models = createMemo(() => data.location.model.list() ?? [])
|
||||
const providers = createMemo(
|
||||
() => new Map((data.location.provider.list(location.ref) ?? []).map((item) => [item.id, item])),
|
||||
)
|
||||
const models = createMemo(() => data.location.model.list(location.ref) ?? [])
|
||||
|
||||
const showExtra = createMemo(() => connected() && !props.providerID)
|
||||
|
||||
|
||||
@@ -327,10 +327,6 @@ export function Prompt(props: PromptProps) {
|
||||
if (!session) return
|
||||
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
||||
if (agent && !args.agent) local.agent.set(agent.id)
|
||||
if (session.model) {
|
||||
local.model.set({ providerID: session.model.providerID, modelID: session.model.id })
|
||||
local.model.variant.set(session.model.variant)
|
||||
}
|
||||
syncedSessionID = sessionID
|
||||
})
|
||||
|
||||
@@ -943,6 +939,25 @@ export function Prompt(props: PromptProps) {
|
||||
await slash.command.run(slash.input)
|
||||
return true
|
||||
}
|
||||
const inputText = expandTrackedPastedText(
|
||||
store.prompt.text,
|
||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||
const ref = store.extmarkToPart.get(extmark.id)
|
||||
if (ref?.type !== "pasted") return []
|
||||
const part = store.prompt.pasted[ref.index]
|
||||
if (!part) return []
|
||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||
}),
|
||||
)
|
||||
const slashHead = parseSlashHead(inputText, /\s/)
|
||||
const isSkill =
|
||||
slashHead !== undefined &&
|
||||
(data.location.skill.list(currentLocation.ref) ?? []).some(
|
||||
(skill) => skill.slash === true && skill.id === slashHead.name,
|
||||
)
|
||||
const isCommand =
|
||||
slashHead !== undefined &&
|
||||
(data.location.command.list(currentLocation.ref) ?? []).some((command) => command.name === slashHead.name)
|
||||
const agent = local.agent.current()
|
||||
if (!agent) return false
|
||||
const selectedModel = local.model.current()
|
||||
@@ -950,6 +965,15 @@ export function Prompt(props: PromptProps) {
|
||||
void promptModelWarning()
|
||||
return false
|
||||
}
|
||||
const usesModel = !props.sessionID || (store.mode !== "shell" && !isSkill)
|
||||
if (usesModel && !local.model.available(selectedModel)) {
|
||||
toast.show({
|
||||
title: "Model unavailable",
|
||||
message: `${selectedModel.providerID}/${selectedModel.modelID} is not available in this session's location`,
|
||||
variant: "warning",
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const variant = local.model.variant.current()
|
||||
let sessionID = props.sessionID
|
||||
@@ -990,17 +1014,6 @@ export function Prompt(props: PromptProps) {
|
||||
session = created
|
||||
}
|
||||
|
||||
const inputText = expandTrackedPastedText(
|
||||
store.prompt.text,
|
||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||
const ref = store.extmarkToPart.get(extmark.id)
|
||||
if (ref?.type !== "pasted") return []
|
||||
const part = store.prompt.pasted[ref.index]
|
||||
if (!part) return []
|
||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||
}),
|
||||
)
|
||||
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const editorSelection = editorContext()
|
||||
@@ -1013,43 +1026,30 @@ export function Prompt(props: PromptProps) {
|
||||
command: inputText,
|
||||
})
|
||||
setStore("mode", "normal")
|
||||
} else if (
|
||||
inputText.startsWith("/") &&
|
||||
(data.location.command.list(currentLocation.current) ?? []).some(
|
||||
(command) => command.name === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||
)
|
||||
) {
|
||||
} else if (slashHead && isCommand) {
|
||||
move.startSubmit()
|
||||
// Parse command from first line, preserve multi-line content in arguments
|
||||
const firstLineEnd = inputText.indexOf("\n")
|
||||
const firstLine = firstLineEnd === -1 ? inputText : inputText.slice(0, firstLineEnd)
|
||||
const [command, ...firstLineArgs] = firstLine.split(" ")
|
||||
const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
|
||||
const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
|
||||
const model = { providerID: selectedModel.providerID, id: selectedModel.modelID, variant }
|
||||
const cancelCommit = local.model.expectCommit(sessionID, model)
|
||||
|
||||
void client.api.session
|
||||
.command({
|
||||
sessionID,
|
||||
command: command.slice(1),
|
||||
arguments: args,
|
||||
command: slashHead.name,
|
||||
arguments: slashHead.arguments,
|
||||
agent: agent.id,
|
||||
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
|
||||
model,
|
||||
files: store.prompt.files,
|
||||
agents: store.prompt.agents,
|
||||
})
|
||||
.catch((error) => {
|
||||
cancelCommit()
|
||||
toast.show({ title: "Failed to run command", message: errorMessage(error), variant: "error" })
|
||||
})
|
||||
} else if (
|
||||
inputText.startsWith("/") &&
|
||||
(data.location.skill.list(currentLocation.current) ?? []).some(
|
||||
(skill) => skill.slash === true && skill.id === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||
)
|
||||
) {
|
||||
} else if (isSkill) {
|
||||
move.startSubmit()
|
||||
void client.api.session.skill({
|
||||
sessionID,
|
||||
skill: inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||
skill: slashHead!.name,
|
||||
})
|
||||
} else {
|
||||
move.startSubmit()
|
||||
@@ -1065,9 +1065,11 @@ export function Prompt(props: PromptProps) {
|
||||
session.model.id !== selectedModel.modelID ||
|
||||
(session.model.variant ?? "default") !== (variant ?? "default")
|
||||
) {
|
||||
await client.api.session.switchModel({
|
||||
sessionID,
|
||||
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
|
||||
const model = { providerID: selectedModel.providerID, id: selectedModel.modelID, variant }
|
||||
const cancelCommit = local.model.expectCommit(sessionID, model)
|
||||
await client.api.session.switchModel({ sessionID, model }).catch((error) => {
|
||||
cancelCommit()
|
||||
throw error
|
||||
})
|
||||
}
|
||||
if (session?.revert) {
|
||||
@@ -1320,10 +1322,7 @@ export function Prompt(props: PromptProps) {
|
||||
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
||||
})()
|
||||
if (!value) return undefined
|
||||
const width =
|
||||
dimensions().width < 44
|
||||
? dimensions().width - 5
|
||||
: Math.min(75, dimensions().width - 4) - 5
|
||||
const width = dimensions().width < 44 ? dimensions().width - 5 : Math.min(75, dimensions().width - 4) - 5
|
||||
return Locale.takeWidth(value, Math.max(1, width)).trimEnd()
|
||||
})
|
||||
const locationLabel = createMemo(() => {
|
||||
|
||||
@@ -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,20 +132,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const agent = createAgent()
|
||||
|
||||
function createModel() {
|
||||
type Selection = ModelPreferenceModel & { variant?: string }
|
||||
const [modelStore, setModelStore] = createStore<
|
||||
ModelPreference & {
|
||||
ready: boolean
|
||||
model: Record<string, ModelPreferenceModel>
|
||||
defaults: Record<string, ModelPreferenceModel | undefined>
|
||||
drafts: Record<string, Selection | undefined>
|
||||
}
|
||||
>({
|
||||
ready: false,
|
||||
model: {},
|
||||
defaults: {},
|
||||
drafts: {},
|
||||
recent: [],
|
||||
favorite: [],
|
||||
variant: {},
|
||||
})
|
||||
|
||||
const repository = createModelPreferenceRepository(path.join(paths.state, "model.json"))
|
||||
const pendingCommits = new Map<string, string>()
|
||||
const commitKey = (value: ModelPreferenceModel & { variant?: string }) =>
|
||||
`${modelPreferenceKey(value)}:${normalizeModelVariant(value.variant) ?? "default"}`
|
||||
const state = {
|
||||
pending: false,
|
||||
}
|
||||
@@ -191,7 +201,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
const model = data.location.model.list()?.[0]
|
||||
const model = models()?.[0]
|
||||
if (!model) return undefined
|
||||
return {
|
||||
providerID: model.providerID,
|
||||
@@ -200,21 +210,109 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const currentModel = createMemo(() => {
|
||||
const sessionID = route.data.type === "session" ? route.data.sessionID : undefined
|
||||
if (sessionID) return modelStore.drafts[sessionID] ?? durableSelection(sessionID)
|
||||
const a = agent.current()
|
||||
return (
|
||||
getFirstValidModel(
|
||||
() => a && modelStore.model[a.id],
|
||||
() => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
|
||||
fallbackModel,
|
||||
) ?? undefined
|
||||
const fallback = getFirstValidModel(
|
||||
() => a && modelStore.defaults[agentModelKey(a.id)],
|
||||
() => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
|
||||
fallbackModel,
|
||||
)
|
||||
return fallback
|
||||
})
|
||||
|
||||
function agentModelKey(agentID: string) {
|
||||
const ref = location.ref ?? data.location.default()
|
||||
return `agent:${JSON.stringify([ref.directory, ref.workspaceID])}:${agentID}`
|
||||
}
|
||||
|
||||
function durableSelection(sessionID: string): Selection | undefined {
|
||||
const model = data.session.get(sessionID)?.model
|
||||
if (!model) return
|
||||
return {
|
||||
providerID: model.providerID,
|
||||
modelID: model.id,
|
||||
variant: normalizeModelVariant(model.variant),
|
||||
}
|
||||
}
|
||||
|
||||
function setDraft(sessionID: string, selection: Selection) {
|
||||
const durable = durableSelection(sessionID)
|
||||
setModelStore(
|
||||
"drafts",
|
||||
sessionID,
|
||||
durable && commitKey(durable) === commitKey(selection) ? undefined : selection,
|
||||
)
|
||||
}
|
||||
|
||||
function select(model: ModelPreferenceModel) {
|
||||
if (route.data.type === "session") {
|
||||
const sessionID = route.data.sessionID
|
||||
const current = modelStore.drafts[sessionID] ?? durableSelection(sessionID)
|
||||
const preferred = normalizeModelVariant(
|
||||
current?.providerID === model.providerID && current.modelID === model.modelID
|
||||
? current.variant
|
||||
: modelStore.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
|
||||
setDraft(sessionID, { ...model, variant })
|
||||
return true
|
||||
}
|
||||
const current = agent.current()
|
||||
if (!current) return false
|
||||
setModelStore("defaults", agentModelKey(current.id), model)
|
||||
return true
|
||||
}
|
||||
|
||||
onCleanup(
|
||||
event.on("session.model.selected", (evt) => {
|
||||
const expected = pendingCommits.get(evt.data.sessionID)
|
||||
if (!expected) return
|
||||
pendingCommits.delete(evt.data.sessionID)
|
||||
const committed = commitKey({
|
||||
providerID: evt.data.model.providerID,
|
||||
modelID: evt.data.model.id,
|
||||
variant: evt.data.model.variant,
|
||||
})
|
||||
if (committed !== expected) return
|
||||
const draft = modelStore.drafts[evt.data.sessionID]
|
||||
if (draft && commitKey(draft) === committed) setModelStore("drafts", evt.data.sessionID, undefined)
|
||||
}),
|
||||
)
|
||||
|
||||
onCleanup(
|
||||
event.on("session.deleted", (evt) => {
|
||||
pendingCommits.delete(evt.data.sessionID)
|
||||
setModelStore("drafts", evt.data.sessionID, undefined)
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
current: currentModel,
|
||||
available(model = currentModel()) {
|
||||
return model ? isModelValid(model) : false
|
||||
},
|
||||
expectCommit(
|
||||
sessionID: string,
|
||||
value: {
|
||||
providerID: string
|
||||
id: string
|
||||
variant?: string
|
||||
},
|
||||
) {
|
||||
const committed = commitKey({ providerID: value.providerID, modelID: value.id, variant: value.variant })
|
||||
pendingCommits.set(sessionID, committed)
|
||||
return () => {
|
||||
if (pendingCommits.get(sessionID) === committed) pendingCommits.delete(sessionID)
|
||||
}
|
||||
},
|
||||
get ready() {
|
||||
return modelStore.ready
|
||||
},
|
||||
get catalogReady() {
|
||||
return models() !== undefined
|
||||
},
|
||||
recent() {
|
||||
return modelStore.recent
|
||||
},
|
||||
@@ -230,30 +328,25 @@ 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()
|
||||
if (!current) return
|
||||
const recent = modelStore.recent
|
||||
const recent = recentModels(current, modelStore.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 })
|
||||
select({ ...val })
|
||||
},
|
||||
cycleFavorite(direction: 1 | -1) {
|
||||
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
|
||||
@@ -279,18 +372,14 @@ 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 })
|
||||
if (!select({ ...next })) return
|
||||
setModelStore("recent", recentModels(next, modelStore.recent))
|
||||
save()
|
||||
},
|
||||
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 (!select(model)) return
|
||||
if (options?.recent) {
|
||||
setModelStore("recent", recentModels(model, modelStore.recent))
|
||||
save()
|
||||
@@ -317,6 +406,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
selected() {
|
||||
const m = currentModel()
|
||||
if (!m) return undefined
|
||||
if (route.data.type === "session") {
|
||||
const selection = modelStore.drafts[route.data.sessionID] ?? durableSelection(route.data.sessionID)
|
||||
if (selection?.providerID === m.providerID && selection.modelID === m.modelID) return selection.variant
|
||||
}
|
||||
return normalizeModelVariant(modelStore.variant[modelPreferenceKey(m)])
|
||||
},
|
||||
current() {
|
||||
@@ -327,14 +420,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
list() {
|
||||
const m = currentModel()
|
||||
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()
|
||||
if (!m) return
|
||||
if (route.data.type === "session") {
|
||||
setDraft(route.data.sessionID, { ...m, variant: normalizeModelVariant(value) })
|
||||
return
|
||||
}
|
||||
setModelStore("variant", modelPreferenceKey(m), normalizeModelVariant(value))
|
||||
save()
|
||||
},
|
||||
|
||||
@@ -361,7 +361,7 @@ export function Session() {
|
||||
|
||||
createEffect(() => {
|
||||
const current = prompt()
|
||||
if (sent || !current || !synced() || !local.model.ready) return
|
||||
if (sent || !current || !synced() || !local.model.ready || !local.model.catalogReady) return
|
||||
if (!local.agent.current() || !local.model.current()) return
|
||||
if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return
|
||||
sent = true
|
||||
|
||||
Reference in New Issue
Block a user