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 |
@@ -416,14 +416,11 @@ export function configured(options?: Options) {
|
||||
function publish<D extends Event.Definition>(definition: D, data: Event.Data<D>, options?: PublishOptions) {
|
||||
return Effect.gen(function* () {
|
||||
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
||||
// Global definitions describe location-independent facts. Never tag
|
||||
// them, so location-filtered subscribers in every location observe them.
|
||||
const location = definition.global
|
||||
? undefined
|
||||
: (options?.location ??
|
||||
(serviceLocation
|
||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||
: undefined))
|
||||
const location =
|
||||
options?.location ??
|
||||
(serviceLocation
|
||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||
: undefined)
|
||||
return yield* publishEvent(
|
||||
definition,
|
||||
{
|
||||
|
||||
@@ -75,13 +75,6 @@ const CountMessage = Bus.ephemeral({
|
||||
count: Schema.Number,
|
||||
},
|
||||
})
|
||||
const GlobalFact = Bus.ephemeral({
|
||||
type: "test.global.fact",
|
||||
global: true,
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
const VersionedMessage = Bus.durable({
|
||||
type: "test.versioned",
|
||||
@@ -160,31 +153,6 @@ describe("Bus", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("publishes global definitions untagged so subscribers in other locations observe them", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const elsewhere = Location.Service.of(
|
||||
location({ directory: AbsolutePath.make("elsewhere"), workspaceID: Workspace.ID.make("wrk_other") }),
|
||||
)
|
||||
const fiber = yield* bus
|
||||
.subscribe([Message, GlobalFact])
|
||||
.pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect,
|
||||
Effect.provideService(Location.Service, elsewhere),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
// Location-tagged events stay invisible to other locations; the global fact reaches them.
|
||||
yield* bus.publish(Message, { text: "tagged" })
|
||||
const event = yield* bus.publish(GlobalFact, { text: "everywhere" })
|
||||
|
||||
expect(event).not.toHaveProperty("location")
|
||||
expect(Array.from(yield* Fiber.join(fiber))).toEqual([event])
|
||||
}),
|
||||
)
|
||||
|
||||
itWithoutLocation.effect("omits location when no location is available", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
@@ -37,7 +37,6 @@ export type DurableDefinition<
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly global?: never
|
||||
readonly data: DataSchema
|
||||
}
|
||||
|
||||
@@ -48,8 +47,6 @@ export type EphemeralDefinition<
|
||||
readonly type: Type
|
||||
readonly durability: "ephemeral"
|
||||
readonly durable?: never
|
||||
/** Global events describe location-independent facts: they are published untagged and reach every location. */
|
||||
readonly global?: boolean
|
||||
readonly data: DataSchema
|
||||
}
|
||||
|
||||
@@ -80,14 +77,13 @@ type Input<Type extends string, Fields extends Readonly<Record<PropertyKey, Sche
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly global?: boolean
|
||||
readonly schema: Fields
|
||||
}
|
||||
|
||||
export function durable<
|
||||
const Type extends string,
|
||||
const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
|
||||
>(input: Omit<Input<Type, Fields>, "global"> & { readonly durable: NonNullable<Input<Type, Fields>["durable"]> }) {
|
||||
>(input: Input<Type, Fields> & { readonly durable: NonNullable<Input<Type, Fields>["durable"]> }) {
|
||||
const data = Schema.Struct(input.schema)
|
||||
const durable = Schema.Struct({
|
||||
aggregateID: DurableEnvelope.fields.aggregateID,
|
||||
@@ -141,7 +137,6 @@ export function ephemeral<
|
||||
type: input.type,
|
||||
durability: "ephemeral" as const,
|
||||
durable: undefined,
|
||||
global: input.global === true,
|
||||
data,
|
||||
})),
|
||||
) satisfies EphemeralDefinition<Type, typeof data>
|
||||
|
||||
@@ -88,12 +88,8 @@ const Updated = ephemeral({
|
||||
type: "integration.updated",
|
||||
schema: {},
|
||||
})
|
||||
// Credentials live in one global store shared by every location, so a
|
||||
// connection change is a location-independent fact: publish it globally so
|
||||
// every active location refreshes its provider catalog.
|
||||
const ConnectionUpdated = ephemeral({
|
||||
type: "integration.connection.updated",
|
||||
global: true,
|
||||
schema: { integrationID: ID },
|
||||
})
|
||||
export const Event = { Updated, ConnectionUpdated, Definitions: inventory(Updated, ConnectionUpdated) }
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
IntegrationOauthConnectOutput,
|
||||
IntegrationOAuthMethod,
|
||||
} from "@opencode-ai/client"
|
||||
import open from "open"
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useClipboard } from "../context/clipboard"
|
||||
import { useData } from "../context/data"
|
||||
@@ -446,19 +445,6 @@ function OAuthAuto(props: {
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "modal",
|
||||
commands: [
|
||||
{
|
||||
bind: "o",
|
||||
title: "Open authorization URL",
|
||||
group: "Dialog",
|
||||
run: () => {
|
||||
open(props.attempt.url).catch(() =>
|
||||
toast.show({
|
||||
message: "Could not open the browser. Copy the URL and continue manually.",
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
bind: "c",
|
||||
title: "Copy authorization details",
|
||||
@@ -516,7 +502,6 @@ function OAuthAuto(props: {
|
||||
instructions={props.attempt.instructions}
|
||||
message="Waiting for authorization..."
|
||||
copy
|
||||
open
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -574,14 +559,7 @@ function OAuthCode(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function OAuthView(props: {
|
||||
title: string
|
||||
url?: string
|
||||
instructions?: string
|
||||
message: string
|
||||
copy?: boolean
|
||||
open?: boolean
|
||||
}) {
|
||||
function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
|
||||
const dialog = useDialog()
|
||||
const theme = useTheme("elevated")
|
||||
return (
|
||||
@@ -605,18 +583,11 @@ function OAuthView(props: {
|
||||
)}
|
||||
</Show>
|
||||
<text fg={theme.text.subdued}>{props.message}</text>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<Show when={props.open}>
|
||||
<text fg={theme.text.default}>
|
||||
o <span style={{ fg: theme.text.subdued }}>open</span>
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={props.copy}>
|
||||
<text fg={theme.text.default}>
|
||||
c <span style={{ fg: theme.text.subdued }}>copy</span>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={props.copy}>
|
||||
<text fg={theme.text.default}>
|
||||
c <span style={{ fg: theme.text.subdued }}>copy</span>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,7 @@ export function Link(props: LinkProps) {
|
||||
open(props.href).catch(() => {})
|
||||
}}
|
||||
>
|
||||
<a href={props.href}>{displayText}</a>
|
||||
{displayText}
|
||||
</text>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user