feat(tui): add v2 terminal interface

This commit is contained in:
Dax Raad 2026-06-26 14:20:47 -04:00
commit e6f660fecf
73 changed files with 3028 additions and 2035 deletions

View file

@ -21,8 +21,13 @@ import type {
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { useEvent } from "./event"
import { createSignal, onCleanup, onMount } from "solid-js"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
export type DataConnectionStatus = "connecting" | "connected" | "reconnecting"
export type DataEvent = V2Event
type DataEventMap = { [T in DataEvent["type"]]: Extract<DataEvent, { type: T }> }
type LocationData = {
agent?: AgentV2Info[]
@ -35,6 +40,11 @@ type LocationData = {
}
type Data = {
connection: {
status: DataConnectionStatus
attempt: number
error?: string
}
session: {
info: Record<string, SessionV2Info>
message: Record<string, SessionMessage[]>
@ -59,6 +69,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
name: "Data",
init: () => {
const [store, setStore] = createStore<Data>({
connection: {
status: "connecting",
attempt: 0,
},
session: {
info: {},
message: {},
@ -72,35 +86,38 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
const sdk = useSDK()
const events = useEvent()
const events = createGlobalEmitter<DataEventMap>()
const [defaultLocation, setDefaultLocation] = createSignal<LocationRef>({
directory: sdk.directory ?? process.cwd(),
})
const messageIndex = new Map<string, Map<string, number>>()
const message = {
update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
update(sessionID: string, fn: (messages: SessionMessage[], index: Map<string, number>) => void) {
setStore(
"session",
"message",
produce((draft) => {
fn((draft[sessionID] ??= []))
fn((draft[sessionID] ??= []), index(sessionID))
}),
)
},
prepend(messages: SessionMessage[], item: SessionMessage) {
if (messages.some((existing) => existing.id === item.id)) return
messages.unshift(item)
append(messages: SessionMessage[], index: Map<string, number>, item: SessionMessage) {
if (index.has(item.id)) return
index.set(item.id, messages.length)
messages.push(item)
},
activeAssistant(messages: SessionMessage[]) {
const item = messages.find((item) => item.type === "assistant" && !item.time.completed)
const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed)
return item?.type === "assistant" ? item : undefined
},
assistant(messages: SessionMessage[], messageID: string) {
const item = messages.find((item) => item.type === "assistant" && item.id === messageID)
assistant(messages: SessionMessage[], index: Map<string, number>, messageID: string) {
const position = index.get(messageID)
const item = position === undefined ? undefined : messages[position]
return item?.type === "assistant" ? item : undefined
},
activeShell(messages: SessionMessage[], callID: string) {
const item = messages.find((item) => item.type === "shell" && item.callID === callID)
const item = messages.findLast((item) => item.type === "shell" && item.callID === callID)
return item?.type === "shell" ? item : undefined
},
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
@ -121,6 +138,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
},
}
function index(sessionID: string) {
const existing = messageIndex.get(sessionID)
if (existing) return existing
const created = new Map<string, number>()
messageIndex.set(sessionID, created)
return created
}
function handleEvent(event: V2Event) {
switch (event.type) {
case "catalog.updated":
@ -130,8 +155,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
])
break
case "session.next.agent.switched":
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "agent-switched",
agent: event.data.agent,
@ -140,8 +167,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.model.switched":
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "model", event.data.model)
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "model-switched",
model: event.data.model,
@ -150,8 +179,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.prompted": {
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "user",
text: event.data.prompt.text,
@ -165,8 +194,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "session.next.prompt.admitted":
break
case "session.next.context.updated":
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "system",
text: event.data.text,
@ -175,8 +204,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.synthetic":
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "synthetic",
sessionID: event.data.sessionID,
@ -186,8 +215,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.shell.started":
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "shell",
callID: event.data.callID,
@ -198,7 +227,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.shell.ended":
message.update(event.data.sessionID, (draft) => {
message.update(event.data.sessionID, (draft, index) => {
const match = message.activeShell(draft, event.data.callID)
if (!match) return
match.output = event.data.output
@ -206,11 +235,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.step.started":
message.update(event.data.sessionID, (draft) => {
if (draft.some((message) => message.id === event.data.assistantMessageID)) return
message.update(event.data.sessionID, (draft, index) => {
if (index.has(event.data.assistantMessageID)) return
const currentAssistant = message.activeAssistant(draft)
if (currentAssistant) currentAssistant.time.completed = event.data.timestamp
message.prepend(draft, {
message.append(draft, index, {
id: event.data.assistantMessageID,
type: "assistant",
agent: event.data.agent,
@ -222,8 +251,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.step.ended":
message.update(event.data.sessionID, (draft) => {
const currentAssistant = message.assistant(draft, event.data.assistantMessageID)
message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
if (!currentAssistant) return
currentAssistant.time.completed = event.data.timestamp
currentAssistant.finish = event.data.finish
@ -234,8 +263,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.step.failed":
message.update(event.data.sessionID, (draft) => {
const currentAssistant = message.assistant(draft, event.data.assistantMessageID)
message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
if (!currentAssistant) return
currentAssistant.time.completed = event.data.timestamp
currentAssistant.finish = "error"
@ -243,8 +272,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.text.started":
message.update(event.data.sessionID, (draft) => {
message.assistant(draft, event.data.assistantMessageID)?.content.push({
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "text",
id: event.data.textID,
text: "",
@ -252,20 +281,20 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.text.delta":
message.update(event.data.sessionID, (draft) => {
const match = message.latestText(message.assistant(draft, event.data.assistantMessageID), event.data.textID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID), event.data.textID)
if (match) match.text += event.data.delta
})
break
case "session.next.text.ended":
message.update(event.data.sessionID, (draft) => {
const match = message.latestText(message.assistant(draft, event.data.assistantMessageID), event.data.textID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID), event.data.textID)
if (match) match.text = event.data.text
})
break
case "session.next.tool.input.started":
message.update(event.data.sessionID, (draft) => {
message.assistant(draft, event.data.assistantMessageID)?.content.push({
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "tool",
id: event.data.callID,
name: event.data.name,
@ -275,20 +304,20 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.tool.input.delta":
message.update(event.data.sessionID, (draft) => {
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
if (match?.state.status === "pending") match.state.input += event.data.delta
})
break
case "session.next.tool.input.ended":
message.update(event.data.sessionID, (draft) => {
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
if (match?.state.status === "pending") match.state.input = event.data.text
})
break
case "session.next.tool.called":
message.update(event.data.sessionID, (draft) => {
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
if (!match) return
match.time.ran = event.data.timestamp
match.provider = event.data.provider
@ -296,16 +325,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.tool.progress":
message.update(event.data.sessionID, (draft) => {
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
if (match?.state.status !== "running") return
match.state.structured = event.data.structured
match.state.content = [...event.data.content]
})
break
case "session.next.tool.success":
message.update(event.data.sessionID, (draft) => {
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
if (match?.state.status !== "running") return
match.state = {
status: "completed",
@ -323,8 +352,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.tool.failed":
message.update(event.data.sessionID, (draft) => {
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
match.state = {
status: "error",
@ -343,32 +372,34 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.next.reasoning.started":
message.update(event.data.sessionID, (draft) => {
message.assistant(draft, event.data.assistantMessageID)?.content.push({
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "reasoning",
id: event.data.reasoningID,
text: "",
providerMetadata: event.data.providerMetadata,
time: { created: event.data.timestamp },
})
})
break
case "session.next.reasoning.delta":
message.update(event.data.sessionID, (draft) => {
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(
message.assistant(draft, event.data.assistantMessageID),
message.assistant(draft, index, event.data.assistantMessageID),
event.data.reasoningID,
)
if (match) match.text += event.data.delta
})
break
case "session.next.reasoning.ended":
message.update(event.data.sessionID, (draft) => {
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(
message.assistant(draft, event.data.assistantMessageID),
message.assistant(draft, index, event.data.assistantMessageID),
event.data.reasoningID,
)
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? event.data.timestamp, completed: event.data.timestamp }
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
}
})
@ -378,8 +409,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "session.next.compaction.delta":
break
case "session.next.compaction.ended":
message.update(event.data.sessionID, (draft) => {
message.prepend(draft, {
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.messageID,
type: "compaction",
reason: event.data.reason,
@ -389,6 +420,41 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "permission.v2.asked":
if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id)) break
setStore("session", "permission", event.data.sessionID, [
...(store.session.permission[event.data.sessionID] ?? []),
event.data,
])
break
case "permission.v2.replied":
setStore(
"session",
"permission",
event.data.sessionID,
(store.session.permission[event.data.sessionID] ?? []).filter(
(request) => request.id !== event.data.requestID,
),
)
break
case "question.v2.asked":
if (store.session.question[event.data.sessionID]?.some((request) => request.id === event.data.id)) break
setStore("session", "question", event.data.sessionID, [
...(store.session.question[event.data.sessionID] ?? []),
event.data,
])
break
case "question.v2.replied":
case "question.v2.rejected":
setStore(
"session",
"question",
event.data.sessionID,
(store.session.question[event.data.sessionID] ?? []).filter(
(request) => request.id !== event.data.requestID,
),
)
break
case "reference.updated":
void result.location.reference.refresh()
break
@ -400,21 +466,27 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
])
break
}
events.emit(event.type, event)
}
onMount(() => {
const unsub = events.subscribe((event, metadata) => {
handleEvent({
...event,
data: event.properties,
location: { directory: metadata.directory, workspaceID: metadata.workspace },
} as V2Event)
})
onCleanup(unsub)
})
const result = {
on: events.on,
listen: events.listen,
connection: {
status() {
return store.connection.status
},
attempt() {
return store.connection.attempt
},
error() {
return store.connection.error
},
},
session: {
list() {
return Object.values(store.session.info).toSorted((a, b) => b.time.updated - a.time.updated)
},
get(sessionID: string) {
return store.session.info[sessionID]
},
@ -423,12 +495,32 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
setStore("session", "info", sessionID, result.data.data)
},
message: {
ids(sessionID: string) {
return (store.session.message[sessionID] ?? []).map((message) => message.id)
},
list(sessionID: string) {
return store.session.message[sessionID]
return store.session.message[sessionID] ?? []
},
get(sessionID: string, messageID: string) {
const messages = store.session.message[sessionID]
const position = messageIndex.get(sessionID)?.get(messageID)
return position === undefined ? undefined : messages?.[position]
},
async refresh(sessionID: string) {
const result = await sdk.client.v2.session.messages({ sessionID }, { throwOnError: true })
setStore("session", "message", sessionID, result.data.data)
setStore("session", "message", sessionID, [])
messageIndex.set(sessionID, new Map())
const response = await sdk.client.v2.session.messages(
{ sessionID, limit: 200, order: "desc" },
{ throwOnError: true },
)
const loaded = response.data.data.toReversed()
const live = store.session.message[sessionID] ?? []
const liveByID = new Map(live.map((message) => [message.id, message]))
const messages = [...loaded.map((message) => liveByID.get(message.id) ?? message), ...live]
.filter((message, index, messages) => messages.findIndex((item) => item.id === message.id) === index)
.toSorted((a, b) => a.time.created - b.time.created)
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
setStore("session", "message", sessionID, messages)
},
},
permission: {
@ -479,7 +571,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.agent.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, "agent", result.data.data)
setStore("location", key, { ...store.location[key], agent: result.data.data })
},
},
command: {
@ -489,7 +581,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.command.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, "command", result.data.data)
setStore("location", key, { ...store.location[key], command: result.data.data })
},
},
integration: {
@ -502,7 +594,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
{ throwOnError: true },
)
const key = locationKey(result.data.location)
setStore("location", key, "integration", result.data.data)
setStore("location", key, { ...store.location[key], integration: result.data.data })
},
},
model: {
@ -512,7 +604,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.model.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, "model", result.data.data)
setStore("location", key, { ...store.location[key], model: result.data.data })
},
},
provider: {
@ -522,7 +614,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.provider.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, "provider", result.data.data)
setStore("location", key, { ...store.location[key], provider: result.data.data })
},
},
reference: {
@ -532,7 +624,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.reference.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, "reference", result.data.data)
setStore("location", key, { ...store.location[key], reference: result.data.data })
},
},
skill: {
@ -542,14 +634,25 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.skill.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, "skill", result.data.data)
setStore("location", key, { ...store.location[key], skill: result.data.data })
},
},
},
}
onMount(() => {
void Promise.allSettled([
async function bootstrap() {
const settled = await Promise.allSettled([
sdk.client.v2.session
.list({ limit: 50, order: "desc" }, { throwOnError: true })
.then((response) =>
setStore(
"session",
"info",
produce((draft) => {
for (const session of response.data.data) draft[session.id] = session
}),
),
),
result.location.refresh(),
result.location.agent.refresh(),
result.location.integration.refresh(),
@ -558,12 +661,57 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
result.location.reference.refresh(),
result.location.command.refresh(),
result.location.skill.refresh(),
]).then((settled) => {
for (const failure of settled.filter((item) => item.status === "rejected"))
console.error("Failed to refresh default location data", failure.reason)
})
])
for (const failure of settled.filter((item) => item.status === "rejected"))
console.error("Failed to refresh default location data", failure.reason)
}
onMount(() => {
const controller = new AbortController()
onCleanup(() => controller.abort())
void (async () => {
while (!controller.signal.aborted) {
const error = await (async () => {
const events = await sdk.client.v2.event.subscribe({
signal: controller.signal,
sseMaxRetryAttempts: 0,
throwOnError: true,
})
const stream = events.stream[Symbol.asyncIterator]()
const first = await stream.next()
if (first.done) return new Error("Event stream disconnected")
setStore("connection", { status: "connected", attempt: 0, error: undefined })
handleEvent(first.value)
await bootstrap()
while (!controller.signal.aborted) {
const event = await stream.next()
if (event.done) return new Error("Event stream disconnected")
handleEvent(event.value)
}
})().catch((error) => error)
if (controller.signal.aborted) return
setStore("connection", {
status: "reconnecting",
attempt: store.connection.status === "connected" ? 1 : store.connection.attempt + 1,
error: error instanceof Error ? error.message : String(error),
})
await wait(250, controller.signal)
}
})()
})
return result
},
})
function wait(delay: number, signal: AbortSignal) {
return new Promise<void>((resolve) => {
const timer = setTimeout(done, delay)
signal.addEventListener("abort", done, { once: true })
function done() {
clearTimeout(timer)
signal.removeEventListener("abort", done)
resolve()
}
})
}

View file

@ -12,6 +12,7 @@ import { readJson, writeJsonAtomic } from "../util/persistence"
import { useTheme } from "./theme"
import { useToast } from "../ui/toast"
import { useRoute } from "./route"
import { useData } from "./data"
export type LocalTheme = {
secondary: RGBA
@ -51,6 +52,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
init: () => {
const sync = useSync()
const data = useData()
const sdk = useSDK()
const toast = useToast()
const theme = useTheme().theme
@ -58,8 +60,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const paths = useTuiPaths()
function isModelValid(model: { providerID: string; modelID: string }) {
const provider = sync.data.provider.find((item) => item.id === model.providerID)
return !!provider?.models[model.modelID]
return !!data.location.model
.list()
?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
}
function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
@ -71,8 +74,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
function createAgent() {
const agents = createMemo(() => sync.data.agent.filter((agent) => agent.mode !== "subagent" && !agent.hidden))
const visibleAgents = createMemo(() => sync.data.agent.filter((agent) => !agent.hidden))
const agents = createMemo(() =>
(data.location.agent.list() ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
)
const visibleAgents = createMemo(() => (data.location.agent.list() ?? []).filter((agent) => !agent.hidden))
const [agentStore, setAgentStore] = createStore({
current: undefined as string | undefined,
})
@ -90,30 +95,30 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return agents()
},
current() {
return agents().find((x) => x.name === agentStore.current) ?? agents().at(0)
return agents().find((agent) => agent.id === agentStore.current) ?? agents().at(0)
},
set(name: string) {
if (!agents().some((x) => x.name === name))
set(id: string) {
if (!agents().some((agent) => agent.id === id))
return toast.show({
variant: "warning",
message: `Agent not found: ${name}`,
message: `Agent not found: ${id}`,
duration: 3000,
})
setAgentStore("current", name)
setAgentStore("current", id)
},
move(direction: 1 | -1) {
batch(() => {
const current = this.current()
if (!current) return
let next = agents().findIndex((x) => x.name === current.name) + direction
let next = agents().findIndex((agent) => agent.id === current.id) + direction
if (next < 0) next = agents().length - 1
if (next >= agents().length) next = 0
const value = agents()[next]
setAgentStore("current", value.name)
setAgentStore("current", value.id)
})
},
color(name: string) {
const index = visibleAgents().findIndex((x) => x.name === name)
color(id: string) {
const index = visibleAgents().findIndex((agent) => agent.id === id)
if (index === -1) return colors()[0]
const agent = visibleAgents()[index]
@ -218,15 +223,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
const provider = sync.data.provider[0]
if (!provider) return undefined
const defaultModel = sync.data.provider_default[provider.id]
const firstModel = Object.values(provider.models)[0]
const model = defaultModel ?? firstModel?.id
const model = data.location.model.list()?.[0]
if (!model) return undefined
return {
providerID: provider.id,
modelID: model,
providerID: model.providerID,
modelID: model.id,
}
})
@ -234,8 +235,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const a = agent.current()
return (
getFirstValidModel(
() => a && modelStore.model[a.name],
() => a && a.model,
() => a && modelStore.model[a.id],
() => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
fallbackModel,
) ?? undefined
)
@ -261,12 +262,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
reasoning: false,
}
}
const provider = sync.data.provider.find((item) => item.id === value.providerID)
const info = provider?.models[value.modelID]
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)
return {
provider: provider?.name ?? value.providerID,
model: info?.name ?? value.modelID,
reasoning: info?.capabilities?.reasoning ?? false,
reasoning: info?.variants.length !== 0,
}
}),
cycle(direction: 1 | -1) {
@ -282,7 +285,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!val) return
const a = agent.current()
if (!a) return
setModelStore("model", a.name, { ...val })
setModelStore("model", a.id, { ...val })
},
cycleFavorite(direction: 1 | -1) {
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
@ -310,7 +313,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!next) return
const a = agent.current()
if (!a) return
setModelStore("model", a.name, { ...next })
setModelStore("model", a.id, { ...next })
setModelStore("recent", recentModels(next, modelStore.recent))
save()
},
@ -326,7 +329,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const a = agent.current()
if (!a) return
setModelStore("model", a.name, model)
setModelStore("model", a.id, model)
if (options?.recent) {
setModelStore("recent", recentModels(model, modelStore.recent))
save()
@ -361,21 +364,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const m = currentModel()
if (!m) return undefined
const key = `${m.providerID}/${m.modelID}`
return modelStore.variant[key]
return modelStore.variant[key] ?? "default"
},
current() {
const v = this.selected()
if (!v) return undefined
if (!this.list().includes(v)) return undefined
return v
if (v !== "default" && this.list().includes(v)) return v
const m = currentModel()!
return (
data.location.model
.list()
?.find((item) => item.providerID === m.providerID && item.id === m.modelID)?.request.variant ?? "default"
)
},
list() {
const m = currentModel()
if (!m) return []
const provider = sync.data.provider.find((item) => item.id === m.providerID)
const info = provider?.models[m.modelID]
if (!info?.variants) return []
return Object.keys(info.variants)
const info = data.location.model
.list()
?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
return info?.variants.map((variant) => variant.id) ?? []
},
set(value: string | undefined) {
const m = currentModel()
@ -394,7 +402,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const index = variants.indexOf(current)
if (index === -1 || index === variants.length - 1) {
this.set(undefined)
this.set(variants[0])
return
}
this.set(variants[index + 1])
@ -521,10 +529,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
createEffect(() => {
const value = agent.current()
if (!value?.model) return
if (isModelValid(value.model)) return
if (isModelValid({ providerID: value.model.providerID, modelID: value.model.id })) return
toast.show({
variant: "warning",
message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
message: `Agent ${value.id}'s configured model ${value.model.providerID}/${value.model.id} is not valid`,
duration: 3000,
})
})