Add skills catalog and sidebar tooling
This commit is contained in:
@@ -5,7 +5,12 @@ import { getLogger } from "../lib/logger"
|
||||
|
||||
const log = getLogger("api")
|
||||
|
||||
const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {}, sessionTasks: {} }
|
||||
const DEFAULT_INSTANCE_DATA: InstanceData = {
|
||||
messageHistory: [],
|
||||
agentModelSelections: {},
|
||||
sessionTasks: {},
|
||||
sessionSkills: {},
|
||||
}
|
||||
|
||||
const [instanceDataMap, setInstanceDataMap] = createSignal<Map<string, InstanceData>>(new Map())
|
||||
const loadPromises = new Map<string, Promise<void>>()
|
||||
@@ -18,6 +23,7 @@ function cloneInstanceData(data?: InstanceData | null): InstanceData {
|
||||
messageHistory: Array.isArray(source.messageHistory) ? [...source.messageHistory] : [],
|
||||
agentModelSelections: { ...(source.agentModelSelections ?? {}) },
|
||||
sessionTasks: { ...(source.sessionTasks ?? {}) },
|
||||
sessionSkills: { ...(source.sessionSkills ?? {}) },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createSession, loadMessages } from "./session-api"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
import { showConfirmDialog } from "./alerts"
|
||||
import { QwenOAuthManager } from "../lib/integrations/qwen-oauth"
|
||||
import { loadSkillDetails } from "./skills"
|
||||
|
||||
const log = getLogger("actions")
|
||||
|
||||
@@ -255,6 +256,46 @@ function buildLanguageSystemInstruction(prompt: string): string | undefined {
|
||||
return "Respond in English unless the user explicitly requests another language."
|
||||
}
|
||||
|
||||
function clampText(value: string, maxChars: number): string {
|
||||
if (value.length <= maxChars) return value
|
||||
return `${value.slice(0, Math.max(0, maxChars - 3))}...`
|
||||
}
|
||||
|
||||
async function buildSkillsSystemInstruction(instanceId: string, sessionId: string): Promise<string | undefined> {
|
||||
const session = sessions().get(instanceId)?.get(sessionId)
|
||||
const selected = session?.skills ?? []
|
||||
if (selected.length === 0) return undefined
|
||||
|
||||
const details = await loadSkillDetails(selected.map((skill) => skill.id))
|
||||
if (details.length === 0) return undefined
|
||||
|
||||
const sections: string[] = []
|
||||
for (const detail of details) {
|
||||
const header = detail.name ? `# Skill: ${detail.name}` : `# Skill: ${detail.id}`
|
||||
const content = detail.content ? clampText(detail.content.trim(), 4000) : ""
|
||||
sections.push(`${header}\n${content}`.trim())
|
||||
}
|
||||
|
||||
const payload = sections.join("\n\n")
|
||||
if (!payload) return undefined
|
||||
return `You have access to the following skills. Follow their instructions when relevant.\n\n${payload}`
|
||||
}
|
||||
|
||||
async function mergeSystemInstructions(
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
prompt: string,
|
||||
): Promise<string | undefined> {
|
||||
const [languageSystem, skillsSystem] = await Promise.all([
|
||||
Promise.resolve(buildLanguageSystemInstruction(prompt)),
|
||||
buildSkillsSystemInstruction(instanceId, sessionId),
|
||||
])
|
||||
if (languageSystem && skillsSystem) {
|
||||
return `${languageSystem}\n\n${skillsSystem}`
|
||||
}
|
||||
return languageSystem || skillsSystem
|
||||
}
|
||||
|
||||
function extractPlainTextFromParts(parts: Array<{ type?: string; text?: unknown; filename?: string }>): string {
|
||||
const segments: string[] = []
|
||||
for (const part of parts) {
|
||||
@@ -821,7 +862,7 @@ async function sendMessage(
|
||||
})
|
||||
|
||||
const providerId = effectiveModel.providerId
|
||||
const languageSystem = buildLanguageSystemInstruction(prompt)
|
||||
const systemMessage = await mergeSystemInstructions(instanceId, sessionId, prompt)
|
||||
if (providerId === "ollama-cloud" || providerId === "qwen-oauth" || providerId === "opencode-zen" || providerId === "zai") {
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
const now = Date.now()
|
||||
@@ -861,7 +902,7 @@ async function sendMessage(
|
||||
sessionId,
|
||||
providerId,
|
||||
effectiveModel.modelId,
|
||||
languageSystem,
|
||||
systemMessage,
|
||||
messageId,
|
||||
assistantMessageId,
|
||||
assistantPartId,
|
||||
@@ -872,7 +913,7 @@ async function sendMessage(
|
||||
sessionId,
|
||||
providerId,
|
||||
effectiveModel.modelId,
|
||||
languageSystem,
|
||||
systemMessage,
|
||||
messageId,
|
||||
assistantMessageId,
|
||||
assistantPartId,
|
||||
@@ -883,7 +924,7 @@ async function sendMessage(
|
||||
sessionId,
|
||||
providerId,
|
||||
effectiveModel.modelId,
|
||||
languageSystem,
|
||||
systemMessage,
|
||||
messageId,
|
||||
assistantMessageId,
|
||||
assistantPartId,
|
||||
@@ -921,7 +962,7 @@ async function sendMessage(
|
||||
sessionId,
|
||||
providerId,
|
||||
effectiveModel.modelId,
|
||||
languageSystem,
|
||||
systemMessage,
|
||||
token.access_token,
|
||||
token.resource_url,
|
||||
messageId,
|
||||
@@ -989,7 +1030,7 @@ async function sendMessage(
|
||||
modelID: effectiveModel.modelId,
|
||||
},
|
||||
}),
|
||||
...(languageSystem && { system: languageSystem }),
|
||||
...(systemMessage && { system: systemMessage }),
|
||||
}
|
||||
|
||||
log.info("sendMessage", {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Session } from "../types/session"
|
||||
import type { Session, Provider, Model } from "../types/session"
|
||||
import type { Message } from "../types/message"
|
||||
|
||||
import { instances } from "./instances"
|
||||
import { preferences, setAgentModelPreference } from "./preferences"
|
||||
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
||||
import { setSessionCompactionState } from "./session-compaction"
|
||||
import {
|
||||
activeSessionId,
|
||||
@@ -32,9 +32,247 @@ import { seedSessionMessagesV2 } from "./message-v2/bridge"
|
||||
import { messageStoreBus } from "./message-v2/bus"
|
||||
import { clearCacheForSession } from "../lib/global-cache"
|
||||
import { getLogger } from "../lib/logger"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
|
||||
const log = getLogger("api")
|
||||
|
||||
type ProviderMap = Map<string, Provider>
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T | null> {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) return null
|
||||
return (await response.json()) as T
|
||||
} catch (error) {
|
||||
log.warn("Failed to fetch provider data", { url, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProviders(base: Provider[], extras: Provider[]): Provider[] {
|
||||
if (extras.length === 0) return base
|
||||
const map: ProviderMap = new Map(base.map((provider) => [provider.id, { ...provider }]))
|
||||
|
||||
for (const extra of extras) {
|
||||
const existing = map.get(extra.id)
|
||||
if (!existing) {
|
||||
map.set(extra.id, extra)
|
||||
continue
|
||||
}
|
||||
|
||||
const modelMap = new Map<string, Model>(existing.models.map((model) => [model.id, model]))
|
||||
for (const model of extra.models) {
|
||||
if (!modelMap.has(model.id)) {
|
||||
modelMap.set(model.id, model)
|
||||
}
|
||||
}
|
||||
existing.models = Array.from(modelMap.values())
|
||||
}
|
||||
|
||||
return Array.from(map.values())
|
||||
}
|
||||
|
||||
const OLLAMA_TOAST_COOLDOWN_MS = 30000
|
||||
let lastOllamaToastAt = 0
|
||||
let lastOllamaToastKey = ""
|
||||
|
||||
function showOllamaToastOnce(key: string, payload: Parameters<typeof showToastNotification>[0]) {
|
||||
const now = Date.now()
|
||||
if (lastOllamaToastKey === key && now - lastOllamaToastAt < OLLAMA_TOAST_COOLDOWN_MS) {
|
||||
return
|
||||
}
|
||||
lastOllamaToastKey = key
|
||||
lastOllamaToastAt = now
|
||||
showToastNotification(payload)
|
||||
}
|
||||
|
||||
async function fetchOllamaCloudProvider(): Promise<Provider | null> {
|
||||
try {
|
||||
const config = await fetchJson<{ config?: { enabled?: boolean } }>("/api/ollama/config")
|
||||
if (config && config.config?.enabled === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await fetch("/api/ollama/models")
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "")
|
||||
showOllamaToastOnce(`ollama-unavailable-${response.status}`, {
|
||||
title: "Ollama Cloud unavailable",
|
||||
message: errorText
|
||||
? `Unable to load Ollama Cloud models (${response.status}). ${errorText}`
|
||||
: "Unable to load Ollama Cloud models. Check that the integration is enabled and the API key is valid.",
|
||||
variant: "warning",
|
||||
duration: 8000,
|
||||
})
|
||||
return null
|
||||
}
|
||||
const data = (await response.json()) as { models?: Array<{ name?: string } | string> }
|
||||
const models = Array.isArray(data?.models) ? data.models : []
|
||||
const modelNames = models
|
||||
.map((model) => (typeof model === "string" ? model : model?.name))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
if (modelNames.length === 0) {
|
||||
showOllamaToastOnce("ollama-empty", {
|
||||
title: "Ollama Cloud models unavailable",
|
||||
message: "Ollama Cloud returned no models. Check your API key and endpoint.",
|
||||
variant: "warning",
|
||||
duration: 8000,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: "ollama-cloud",
|
||||
name: "Ollama Cloud",
|
||||
models: modelNames.map((name) => ({
|
||||
id: name,
|
||||
name,
|
||||
providerId: "ollama-cloud",
|
||||
})),
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("Failed to fetch Ollama Cloud models", { error })
|
||||
showOllamaToastOnce("ollama-fetch-error", {
|
||||
title: "Ollama Cloud unavailable",
|
||||
message: "Unable to load Ollama Cloud models. Check that the integration is enabled and the API key is valid.",
|
||||
variant: "warning",
|
||||
duration: 8000,
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredQwenToken():
|
||||
| { access_token: string; expires_in: number; created_at: number }
|
||||
| null {
|
||||
if (typeof window === "undefined") return null
|
||||
try {
|
||||
const raw = window.localStorage.getItem("qwen_oauth_token")
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isQwenTokenValid(token: { expires_in: number; created_at: number } | null): boolean {
|
||||
if (!token) return false
|
||||
const createdAt = token.created_at > 1e12 ? Math.floor(token.created_at / 1000) : token.created_at
|
||||
const expiresAt = (createdAt + token.expires_in) * 1000 - 300000
|
||||
return Date.now() < expiresAt
|
||||
}
|
||||
|
||||
async function fetchQwenOAuthProvider(): Promise<Provider | null> {
|
||||
const token = getStoredQwenToken()
|
||||
if (!isQwenTokenValid(token)) return null
|
||||
|
||||
// Use actual Qwen model IDs that work with the DashScope API
|
||||
const qwenModels: Model[] = [
|
||||
{
|
||||
id: "qwen-coder-plus-latest",
|
||||
name: "Qwen Coder Plus (OAuth)",
|
||||
providerId: "qwen-oauth",
|
||||
limit: { context: 131072, output: 16384 },
|
||||
},
|
||||
{
|
||||
id: "qwen-turbo-latest",
|
||||
name: "Qwen Turbo (OAuth)",
|
||||
providerId: "qwen-oauth",
|
||||
limit: { context: 131072, output: 8192 },
|
||||
},
|
||||
{
|
||||
id: "qwen-plus-latest",
|
||||
name: "Qwen Plus (OAuth)",
|
||||
providerId: "qwen-oauth",
|
||||
limit: { context: 131072, output: 8192 },
|
||||
},
|
||||
{
|
||||
id: "qwen-max-latest",
|
||||
name: "Qwen Max (OAuth)",
|
||||
providerId: "qwen-oauth",
|
||||
limit: { context: 32768, output: 8192 },
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: "qwen-oauth",
|
||||
name: "Qwen OAuth",
|
||||
models: qwenModels,
|
||||
defaultModelId: "qwen-coder-plus-latest",
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOpenCodeZenProvider(): Promise<Provider | null> {
|
||||
const data = await fetchJson<{ models?: Array<{ id: string; name: string; limit?: Model["limit"]; cost?: Model["cost"] }> }>(
|
||||
"/api/opencode-zen/models",
|
||||
)
|
||||
const models = Array.isArray(data?.models) ? data?.models ?? [] : []
|
||||
if (models.length === 0) return null
|
||||
|
||||
return {
|
||||
id: "opencode-zen",
|
||||
name: "OpenCode Zen",
|
||||
models: models.map((model) => ({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
providerId: "opencode-zen",
|
||||
limit: model.limit,
|
||||
cost: model.cost,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchZAIProvider(): Promise<Provider | null> {
|
||||
try {
|
||||
const config = await fetchJson<{ config?: { enabled?: boolean } }>("/api/zai/config")
|
||||
if (!config?.config?.enabled) return null
|
||||
|
||||
const data = await fetchJson<{ models?: Array<{ name: string; provider: string }> }>("/api/zai/models")
|
||||
const models = Array.isArray(data?.models) ? data.models : []
|
||||
if (models.length === 0) return null
|
||||
|
||||
return {
|
||||
id: "zai",
|
||||
name: "Z.AI Coding Plan",
|
||||
models: models.map((model) => ({
|
||||
id: model.name,
|
||||
name: model.name.toUpperCase(),
|
||||
providerId: "zai",
|
||||
limit: { context: 131072, output: 8192 },
|
||||
})),
|
||||
defaultModelId: "glm-4.7",
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("Failed to fetch Z.AI models", { error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchExtraProviders(): Promise<Provider[]> {
|
||||
const [ollama, zen, qwen, zai] = await Promise.all([
|
||||
fetchOllamaCloudProvider(),
|
||||
fetchOpenCodeZenProvider(),
|
||||
fetchQwenOAuthProvider(),
|
||||
fetchZAIProvider(),
|
||||
])
|
||||
return [ollama, zen, qwen, zai].filter((provider): provider is Provider => Boolean(provider))
|
||||
}
|
||||
|
||||
function removeDuplicateProviders(base: Provider[], extras: Provider[]): Provider[] {
|
||||
const extraModelIds = new Set(extras.flatMap((provider) => provider.models.map((model) => model.id)))
|
||||
if (!extras.some((provider) => provider.id === "opencode-zen")) {
|
||||
return base
|
||||
}
|
||||
|
||||
return base.filter((provider) => {
|
||||
if (provider.id === "opencode-zen") return false
|
||||
if (provider.id === "opencode" && provider.models.every((model) => extraModelIds.has(model.id))) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
interface SessionForkResponse {
|
||||
id: string
|
||||
title?: string
|
||||
@@ -84,30 +322,38 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
await ensureInstanceConfigLoaded(instanceId)
|
||||
const instanceData = getInstanceConfig(instanceId)
|
||||
const sessionTasks = instanceData.sessionTasks || {}
|
||||
const sessionSkills = instanceData.sessionSkills || {}
|
||||
|
||||
for (const apiSession of response.data) {
|
||||
const existingSession = existingSessions?.get(apiSession.id)
|
||||
|
||||
const existingModel = existingSession?.model ?? { providerId: "", modelId: "" }
|
||||
const hasUserSelectedModel = existingModel.providerId && existingModel.modelId
|
||||
const apiModel = (apiSession as any).model?.providerID && (apiSession as any).model?.modelID
|
||||
? { providerId: (apiSession as any).model.providerID, modelId: (apiSession as any).model.modelID }
|
||||
: { providerId: "", modelId: "" }
|
||||
|
||||
sessionMap.set(apiSession.id, {
|
||||
id: apiSession.id,
|
||||
instanceId,
|
||||
title: apiSession.title || "Untitled",
|
||||
parentId: apiSession.parentID || null,
|
||||
agent: existingSession?.agent ?? "",
|
||||
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
||||
agent: existingSession?.agent ?? (apiSession as any).agent ?? "",
|
||||
model: hasUserSelectedModel ? existingModel : apiModel,
|
||||
version: apiSession.version,
|
||||
time: {
|
||||
...apiSession.time,
|
||||
},
|
||||
revert: apiSession.revert
|
||||
? {
|
||||
messageID: apiSession.revert.messageID,
|
||||
partID: apiSession.revert.partID,
|
||||
snapshot: apiSession.revert.snapshot,
|
||||
diff: apiSession.revert.diff,
|
||||
}
|
||||
messageID: apiSession.revert.messageID,
|
||||
partID: apiSession.revert.partID,
|
||||
snapshot: apiSession.revert.snapshot,
|
||||
diff: apiSession.revert.diff,
|
||||
}
|
||||
: undefined,
|
||||
tasks: sessionTasks[apiSession.id] || [],
|
||||
skills: sessionSkills[apiSession.id] || [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,7 +399,11 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession(instanceId: string, agent?: string): Promise<Session> {
|
||||
async function createSession(
|
||||
instanceId: string,
|
||||
agent?: string,
|
||||
options?: { skipAutoCleanup?: boolean },
|
||||
): Promise<Session> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
throw new Error("Instance not ready")
|
||||
@@ -163,10 +413,12 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
||||
const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
|
||||
const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")
|
||||
|
||||
const agentModelPreference = await getAgentModelPreference(instanceId, selectedAgent)
|
||||
const defaultModel = await getDefaultModel(instanceId, selectedAgent)
|
||||
const sessionModel = agentModelPreference || defaultModel
|
||||
|
||||
if (selectedAgent && isModelValid(instanceId, defaultModel)) {
|
||||
await setAgentModelPreference(instanceId, selectedAgent, defaultModel)
|
||||
if (selectedAgent && isModelValid(instanceId, sessionModel) && !agentModelPreference) {
|
||||
await setAgentModelPreference(instanceId, selectedAgent, sessionModel)
|
||||
}
|
||||
|
||||
setLoading((prev) => {
|
||||
@@ -189,18 +441,19 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
||||
title: response.data.title || "New Session",
|
||||
parentId: null,
|
||||
agent: selectedAgent,
|
||||
model: defaultModel,
|
||||
model: sessionModel,
|
||||
skills: [],
|
||||
version: response.data.version,
|
||||
time: {
|
||||
...response.data.time,
|
||||
},
|
||||
revert: response.data.revert
|
||||
? {
|
||||
messageID: response.data.revert.messageID,
|
||||
partID: response.data.revert.partID,
|
||||
snapshot: response.data.revert.snapshot,
|
||||
diff: response.data.revert.diff,
|
||||
}
|
||||
messageID: response.data.revert.messageID,
|
||||
partID: response.data.revert.partID,
|
||||
snapshot: response.data.revert.snapshot,
|
||||
diff: response.data.revert.diff,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
@@ -243,7 +496,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
||||
return next
|
||||
})
|
||||
|
||||
if (preferences().autoCleanupBlankSessions) {
|
||||
if (preferences().autoCleanupBlankSessions && !options?.skipAutoCleanup) {
|
||||
await cleanupBlankSessions(instanceId, session.id)
|
||||
}
|
||||
|
||||
@@ -288,26 +541,33 @@ async function forkSession(
|
||||
throw new Error("Failed to fork session: No data returned")
|
||||
}
|
||||
|
||||
const sourceSession = sessions().get(instanceId)?.get(sourceSessionId)
|
||||
const sourceModel = sourceSession?.model ?? { providerId: "", modelId: "" }
|
||||
const sourceSkills = sourceSession?.skills ?? []
|
||||
|
||||
const info = response.data as SessionForkResponse
|
||||
const forkedSession = {
|
||||
id: info.id,
|
||||
instanceId,
|
||||
title: info.title || "Forked Session",
|
||||
parentId: info.parentID || sourceSessionId, // Fallback to source session to ensure parent-child relationship
|
||||
agent: info.agent || "",
|
||||
model: {
|
||||
providerId: info.model?.providerID || "",
|
||||
modelId: info.model?.modelID || "",
|
||||
},
|
||||
agent: info.agent || sourceSession?.agent || "",
|
||||
model: sourceModel.providerId && sourceModel.modelId
|
||||
? { providerId: sourceModel.providerId, modelId: sourceModel.modelId }
|
||||
: {
|
||||
providerId: info.model?.providerID || "",
|
||||
modelId: info.model?.modelID || "",
|
||||
},
|
||||
skills: sourceSkills,
|
||||
version: "0",
|
||||
time: info.time ? { ...info.time } : { created: Date.now(), updated: Date.now() },
|
||||
revert: info.revert
|
||||
? {
|
||||
messageID: info.revert.messageID,
|
||||
partID: info.revert.partID,
|
||||
snapshot: info.revert.snapshot,
|
||||
diff: info.revert.diff,
|
||||
}
|
||||
messageID: info.revert.messageID,
|
||||
partID: info.revert.partID,
|
||||
snapshot: info.revert.snapshot,
|
||||
diff: info.revert.diff,
|
||||
}
|
||||
: undefined,
|
||||
} as unknown as Session
|
||||
|
||||
@@ -437,9 +697,9 @@ async function fetchAgents(instanceId: string): Promise<void> {
|
||||
mode: agent.mode,
|
||||
model: agent.model?.modelID
|
||||
? {
|
||||
providerId: agent.model.providerID || "",
|
||||
modelId: agent.model.modelID,
|
||||
}
|
||||
providerId: agent.model.providerID || "",
|
||||
modelId: agent.model.modelID,
|
||||
}
|
||||
: undefined,
|
||||
}))
|
||||
|
||||
@@ -477,9 +737,15 @@ async function fetchProviders(instanceId: string): Promise<void> {
|
||||
})),
|
||||
}))
|
||||
|
||||
const filteredBaseProviders = providerList.filter((provider) => provider.id !== "zai")
|
||||
|
||||
const extraProviders = await fetchExtraProviders()
|
||||
const baseProviders = removeDuplicateProviders(filteredBaseProviders, extraProviders)
|
||||
const mergedProviders = mergeProviders(baseProviders, extraProviders)
|
||||
|
||||
setProviders((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(instanceId, providerList)
|
||||
next.set(instanceId, mergedProviders)
|
||||
return next
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -588,10 +854,13 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
||||
if (nextInstanceSessions) {
|
||||
const existingSession = nextInstanceSessions.get(sessionId)
|
||||
if (existingSession) {
|
||||
const currentModel = existingSession.model
|
||||
const hasUserSelectedModel = currentModel.providerId && currentModel.modelId
|
||||
|
||||
const updatedSession = {
|
||||
...existingSession,
|
||||
agent: agentName || existingSession.agent,
|
||||
model: providerID && modelID ? { providerId: providerID, modelId: modelID } : existingSession.model,
|
||||
model: hasUserSelectedModel ? currentModel : (providerID && modelID ? { providerId: providerID, modelId: modelID } : currentModel),
|
||||
}
|
||||
const updatedInstanceSessions = new Map(nextInstanceSessions)
|
||||
updatedInstanceSessions.set(sessionId, updatedSession)
|
||||
@@ -611,8 +880,12 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
||||
|
||||
const sessionForV2 = sessions().get(instanceId)?.get(sessionId) ?? {
|
||||
id: sessionId,
|
||||
title: session?.title,
|
||||
instanceId,
|
||||
parentId: session?.parentId ?? null,
|
||||
agent: "",
|
||||
model: { providerId: "", modelId: "" },
|
||||
version: "0",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
revert: session?.revert,
|
||||
}
|
||||
seedSessionMessagesV2(instanceId, sessionForV2, messages, messagesInfo)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
import type { Session, Agent, Provider } from "../types/session"
|
||||
import type { Session, Agent, Provider, SkillSelection } from "../types/session"
|
||||
import { deleteSession, loadMessages } from "./session-api"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
import { messageStoreBus } from "./message-v2/bus"
|
||||
@@ -164,14 +164,19 @@ async function persistSessionTasks(instanceId: string) {
|
||||
if (!instanceSessions) return
|
||||
|
||||
const sessionTasks: Record<string, any[]> = {}
|
||||
const sessionSkills: Record<string, SkillSelection[]> = {}
|
||||
for (const [sessionId, session] of instanceSessions) {
|
||||
if (session.tasks && session.tasks.length > 0) {
|
||||
sessionTasks[sessionId] = session.tasks
|
||||
}
|
||||
if (session.skills && session.skills.length > 0) {
|
||||
sessionSkills[sessionId] = session.skills
|
||||
}
|
||||
}
|
||||
|
||||
await updateInstanceConfig(instanceId, (draft) => {
|
||||
draft.sessionTasks = sessionTasks
|
||||
draft.sessionSkills = sessionSkills
|
||||
})
|
||||
} catch (error) {
|
||||
log.error("Failed to persist session tasks", error)
|
||||
@@ -264,6 +269,17 @@ function getSessionFamily(instanceId: string, parentId: string): Session[] {
|
||||
return [parent, ...children]
|
||||
}
|
||||
|
||||
function getSessionSkills(instanceId: string, sessionId: string): SkillSelection[] {
|
||||
const session = sessions().get(instanceId)?.get(sessionId)
|
||||
return session?.skills ?? []
|
||||
}
|
||||
|
||||
function setSessionSkills(instanceId: string, sessionId: string, skills: SkillSelection[]): void {
|
||||
withSession(instanceId, sessionId, (session) => {
|
||||
session.skills = skills
|
||||
})
|
||||
}
|
||||
|
||||
function isSessionBusy(instanceId: string, sessionId: string): boolean {
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
if (!instanceSessions) return false
|
||||
@@ -283,8 +299,13 @@ async function isBlankSession(session: Session, instanceId: string, fetchIfNeede
|
||||
const created = session.time?.created || 0
|
||||
const updated = session.time?.updated || 0
|
||||
const hasChildren = getChildSessions(instanceId, session.id).length > 0
|
||||
const hasTasks = Boolean(session.tasks && session.tasks.length > 0)
|
||||
const isFreshSession = created === updated && !hasChildren
|
||||
|
||||
if (hasTasks) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Common short-circuit: fresh sessions without children
|
||||
if (!fetchIfNeeded) {
|
||||
return isFreshSession
|
||||
@@ -423,4 +444,6 @@ export {
|
||||
getSessionInfo,
|
||||
isBlankSession,
|
||||
cleanupBlankSessions,
|
||||
getSessionSkills,
|
||||
setSessionSkills,
|
||||
}
|
||||
|
||||
75
packages/ui/src/stores/skills.ts
Normal file
75
packages/ui/src/stores/skills.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import type { SkillCatalogResponse, SkillDescriptor, SkillDetail } from "../../../server/src/api-types"
|
||||
import { serverApi } from "../lib/api-client"
|
||||
import { getLogger } from "../lib/logger"
|
||||
|
||||
const log = getLogger("skills")
|
||||
|
||||
const [catalog, setCatalog] = createSignal<SkillDescriptor[]>([])
|
||||
const [catalogLoading, setCatalogLoading] = createSignal(false)
|
||||
const [catalogError, setCatalogError] = createSignal<string | null>(null)
|
||||
|
||||
const detailCache = new Map<string, SkillDetail>()
|
||||
const detailPromises = new Map<string, Promise<SkillDetail>>()
|
||||
|
||||
async function loadCatalog(): Promise<SkillDescriptor[]> {
|
||||
if (catalog().length > 0) return catalog()
|
||||
if (catalogLoading()) return catalog()
|
||||
setCatalogLoading(true)
|
||||
setCatalogError(null)
|
||||
|
||||
try {
|
||||
const response: SkillCatalogResponse = await serverApi.fetchSkillsCatalog()
|
||||
const skills = Array.isArray(response.skills) ? response.skills : []
|
||||
setCatalog(skills)
|
||||
return skills
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load skills"
|
||||
setCatalogError(message)
|
||||
log.warn("Failed to load skills catalog", error)
|
||||
return []
|
||||
} finally {
|
||||
setCatalogLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkillDetail(id: string): Promise<SkillDetail | null> {
|
||||
if (!id) return null
|
||||
if (detailCache.has(id)) return detailCache.get(id) || null
|
||||
const pending = detailPromises.get(id)
|
||||
if (pending) return pending
|
||||
|
||||
const promise = serverApi
|
||||
.fetchSkillDetail(id)
|
||||
.then((detail) => {
|
||||
detailCache.set(id, detail)
|
||||
return detail
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn("Failed to load skill detail", { id, error })
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
detailPromises.delete(id)
|
||||
})
|
||||
|
||||
detailPromises.set(id, promise as Promise<SkillDetail>)
|
||||
return promise
|
||||
}
|
||||
|
||||
async function loadSkillDetails(ids: string[]): Promise<SkillDetail[]> {
|
||||
const uniqueIds = Array.from(new Set(ids.filter(Boolean)))
|
||||
if (uniqueIds.length === 0) return []
|
||||
|
||||
const results = await Promise.all(uniqueIds.map((id) => loadSkillDetail(id)))
|
||||
return results.filter((detail): detail is SkillDetail => Boolean(detail))
|
||||
}
|
||||
|
||||
export {
|
||||
catalog,
|
||||
catalogLoading,
|
||||
catalogError,
|
||||
loadCatalog,
|
||||
loadSkillDetail,
|
||||
loadSkillDetails,
|
||||
}
|
||||
Reference in New Issue
Block a user