feat: add instance config provider and map storage ids
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AgentModelSelection,
|
||||||
AgentModelSelections,
|
AgentModelSelections,
|
||||||
ConfigFile,
|
ConfigFile,
|
||||||
ModelPreference,
|
ModelPreference,
|
||||||
@@ -107,6 +108,7 @@ export type WorkspaceFileSearchResponse = FileSystemEntry[]
|
|||||||
|
|
||||||
export interface InstanceData {
|
export interface InstanceData {
|
||||||
messageHistory: string[]
|
messageHistory: string[]
|
||||||
|
agentModelSelections: AgentModelSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BinaryRecord {
|
export interface BinaryRecord {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const PreferencesSchema = z.object({
|
|||||||
lastUsedBinary: z.string().optional(),
|
lastUsedBinary: z.string().optional(),
|
||||||
environmentVariables: z.record(z.string()).default({}),
|
environmentVariables: z.record(z.string()).default({}),
|
||||||
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
||||||
agentModelSelections: AgentModelSelectionsSchema.default({}),
|
|
||||||
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
||||||
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||||
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser })
|
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser })
|
||||||
registerMetaRoutes(app, { serverMeta: deps.serverMeta })
|
registerMetaRoutes(app, { serverMeta: deps.serverMeta })
|
||||||
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient })
|
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient })
|
||||||
registerStorageRoutes(app, { instanceStore: deps.instanceStore, eventBus: deps.eventBus })
|
registerStorageRoutes(app, {
|
||||||
|
instanceStore: deps.instanceStore,
|
||||||
|
eventBus: deps.eventBus,
|
||||||
|
workspaceManager: deps.workspaceManager,
|
||||||
|
})
|
||||||
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,36 @@ import { FastifyInstance } from "fastify"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { InstanceStore } from "../../storage/instance-store"
|
import { InstanceStore } from "../../storage/instance-store"
|
||||||
import { EventBus } from "../../events/bus"
|
import { EventBus } from "../../events/bus"
|
||||||
|
import { ModelPreferenceSchema } from "../../config/schema"
|
||||||
import type { InstanceData } from "../../api-types"
|
import type { InstanceData } from "../../api-types"
|
||||||
|
import { WorkspaceManager } from "../../workspaces/manager"
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
instanceStore: InstanceStore
|
instanceStore: InstanceStore
|
||||||
eventBus: EventBus
|
eventBus: EventBus
|
||||||
|
workspaceManager: WorkspaceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstanceDataSchema = z.object({
|
const InstanceDataSchema = z.object({
|
||||||
messageHistory: z.array(z.string()).default([]),
|
messageHistory: z.array(z.string()).default([]),
|
||||||
|
agentModelSelections: z.record(z.string(), ModelPreferenceSchema).default({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const EMPTY_INSTANCE_DATA: InstanceData = {
|
const EMPTY_INSTANCE_DATA: InstanceData = {
|
||||||
messageHistory: [],
|
messageHistory: [],
|
||||||
|
agentModelSelections: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
|
const resolveStorageKey = (instanceId: string): string => {
|
||||||
|
const workspace = deps.workspaceManager.get(instanceId)
|
||||||
|
return workspace?.path ?? instanceId
|
||||||
|
}
|
||||||
|
|
||||||
app.get<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
|
app.get<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const data = await deps.instanceStore.read(request.params.id)
|
const storageId = resolveStorageKey(request.params.id)
|
||||||
|
const data = await deps.instanceStore.read(storageId)
|
||||||
return data
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reply.code(500)
|
reply.code(500)
|
||||||
@@ -31,7 +42,8 @@ export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
app.put<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
|
app.put<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = InstanceDataSchema.parse(request.body ?? {})
|
const body = InstanceDataSchema.parse(request.body ?? {})
|
||||||
await deps.instanceStore.write(request.params.id, body)
|
const storageId = resolveStorageKey(request.params.id)
|
||||||
|
await deps.instanceStore.write(storageId, body)
|
||||||
deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: body })
|
deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: body })
|
||||||
reply.code(204)
|
reply.code(204)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,7 +54,8 @@ export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
|
|
||||||
app.delete<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
|
app.delete<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
await deps.instanceStore.delete(request.params.id)
|
const storageId = resolveStorageKey(request.params.id)
|
||||||
|
await deps.instanceStore.delete(storageId)
|
||||||
deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: EMPTY_INSTANCE_DATA })
|
deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: EMPTY_INSTANCE_DATA })
|
||||||
reply.code(204)
|
reply.code(204)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { InstanceData } from "../api-types"
|
|||||||
|
|
||||||
const DEFAULT_INSTANCE_DATA: InstanceData = {
|
const DEFAULT_INSTANCE_DATA: InstanceData = {
|
||||||
messageHistory: [],
|
messageHistory: [],
|
||||||
|
agentModelSelections: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InstanceStore {
|
export class InstanceStore {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type ConfigData = AppConfig
|
|||||||
|
|
||||||
const DEFAULT_INSTANCE_DATA: InstanceData = {
|
const DEFAULT_INSTANCE_DATA: InstanceData = {
|
||||||
messageHistory: [],
|
messageHistory: [],
|
||||||
|
agentModelSelections: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDeepEqual(a: unknown, b: unknown): boolean {
|
function isDeepEqual(a: unknown, b: unknown): boolean {
|
||||||
@@ -150,9 +151,11 @@ export class ServerStorage {
|
|||||||
private normalizeInstanceData(data?: InstanceData | null): InstanceData {
|
private normalizeInstanceData(data?: InstanceData | null): InstanceData {
|
||||||
const source = data ?? DEFAULT_INSTANCE_DATA
|
const source = data ?? DEFAULT_INSTANCE_DATA
|
||||||
const messageHistory = Array.isArray(source.messageHistory) ? [...source.messageHistory] : []
|
const messageHistory = Array.isArray(source.messageHistory) ? [...source.messageHistory] : []
|
||||||
|
const agentModelSelections = { ...(source.agentModelSelections ?? {}) }
|
||||||
return {
|
return {
|
||||||
...source,
|
...source,
|
||||||
messageHistory,
|
messageHistory,
|
||||||
|
agentModelSelections,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { render } from "solid-js/web"
|
|||||||
import App from "./App"
|
import App from "./App"
|
||||||
import { ThemeProvider } from "./lib/theme"
|
import { ThemeProvider } from "./lib/theme"
|
||||||
import { ConfigProvider } from "./stores/preferences"
|
import { ConfigProvider } from "./stores/preferences"
|
||||||
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
|
|
||||||
@@ -14,9 +15,11 @@ if (!root) {
|
|||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<ThemeProvider>
|
<InstanceConfigProvider>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
</ThemeProvider>
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</InstanceConfigProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
),
|
),
|
||||||
root,
|
root,
|
||||||
|
|||||||
138
packages/ui/src/stores/instance-config.tsx
Normal file
138
packages/ui/src/stores/instance-config.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { createContext, createMemo, createSignal, onCleanup, type Accessor, type ParentComponent, useContext } from "solid-js"
|
||||||
|
import type { InstanceData } from "../../../cli/src/api-types"
|
||||||
|
import { storage } from "../lib/storage"
|
||||||
|
|
||||||
|
const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {} }
|
||||||
|
|
||||||
|
const [instanceDataMap, setInstanceDataMap] = createSignal<Map<string, InstanceData>>(new Map())
|
||||||
|
const loadPromises = new Map<string, Promise<void>>()
|
||||||
|
const instanceSubscriptions = new Map<string, () => void>()
|
||||||
|
|
||||||
|
function cloneInstanceData(data?: InstanceData | null): InstanceData {
|
||||||
|
const source = data ?? DEFAULT_INSTANCE_DATA
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
messageHistory: Array.isArray(source.messageHistory) ? [...source.messageHistory] : [],
|
||||||
|
agentModelSelections: { ...(source.agentModelSelections ?? {}) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachSubscription(instanceId: string) {
|
||||||
|
if (instanceSubscriptions.has(instanceId)) return
|
||||||
|
const unsubscribe = storage.onInstanceDataChanged(instanceId, (data) => {
|
||||||
|
setInstanceData(instanceId, data)
|
||||||
|
})
|
||||||
|
instanceSubscriptions.set(instanceId, unsubscribe)
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachSubscription(instanceId: string) {
|
||||||
|
const unsubscribe = instanceSubscriptions.get(instanceId)
|
||||||
|
if (!unsubscribe) return
|
||||||
|
unsubscribe()
|
||||||
|
instanceSubscriptions.delete(instanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInstanceData(instanceId: string, data: InstanceData) {
|
||||||
|
setInstanceDataMap((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(instanceId, cloneInstanceData(data))
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureInstanceConfig(instanceId: string): Promise<void> {
|
||||||
|
if (!instanceId) return
|
||||||
|
if (instanceDataMap().has(instanceId)) return
|
||||||
|
if (loadPromises.has(instanceId)) {
|
||||||
|
await loadPromises.get(instanceId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const promise = storage
|
||||||
|
.loadInstanceData(instanceId)
|
||||||
|
.then((data) => {
|
||||||
|
setInstanceData(instanceId, data)
|
||||||
|
attachSubscription(instanceId)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("Failed to load instance data:", error)
|
||||||
|
setInstanceData(instanceId, DEFAULT_INSTANCE_DATA)
|
||||||
|
attachSubscription(instanceId)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loadPromises.delete(instanceId)
|
||||||
|
})
|
||||||
|
loadPromises.set(instanceId, promise)
|
||||||
|
await promise
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateInstanceConfig(instanceId: string, mutator: (draft: InstanceData) => void): Promise<void> {
|
||||||
|
if (!instanceId) return
|
||||||
|
await ensureInstanceConfig(instanceId)
|
||||||
|
const current = instanceDataMap().get(instanceId) ?? DEFAULT_INSTANCE_DATA
|
||||||
|
const draft = cloneInstanceData(current)
|
||||||
|
mutator(draft)
|
||||||
|
try {
|
||||||
|
await storage.saveInstanceData(instanceId, draft)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to persist instance data:", error)
|
||||||
|
}
|
||||||
|
setInstanceData(instanceId, draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstanceConfig(instanceId: string): InstanceData {
|
||||||
|
return instanceDataMap().get(instanceId) ?? DEFAULT_INSTANCE_DATA
|
||||||
|
}
|
||||||
|
|
||||||
|
function useInstanceConfig(instanceId: string): Accessor<InstanceData> {
|
||||||
|
const context = useContext(InstanceConfigContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useInstanceConfig must be used within InstanceConfigProvider")
|
||||||
|
}
|
||||||
|
return createMemo(() => instanceDataMap().get(instanceId) ?? DEFAULT_INSTANCE_DATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInstanceConfig(instanceId: string): void {
|
||||||
|
setInstanceDataMap((prev) => {
|
||||||
|
if (!prev.has(instanceId)) return prev
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.delete(instanceId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
detachSubscription(instanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstanceConfigContextValue {
|
||||||
|
getInstanceConfig: typeof getInstanceConfig
|
||||||
|
ensureInstanceConfig: typeof ensureInstanceConfig
|
||||||
|
updateInstanceConfig: typeof updateInstanceConfig
|
||||||
|
clearInstanceConfig: typeof clearInstanceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceConfigContext = createContext<InstanceConfigContextValue>()
|
||||||
|
|
||||||
|
const contextValue: InstanceConfigContextValue = {
|
||||||
|
getInstanceConfig,
|
||||||
|
ensureInstanceConfig,
|
||||||
|
updateInstanceConfig,
|
||||||
|
clearInstanceConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceConfigProvider: ParentComponent = (props) => {
|
||||||
|
onCleanup(() => {
|
||||||
|
for (const unsubscribe of instanceSubscriptions.values()) {
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
instanceSubscriptions.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
return <InstanceConfigContext.Provider value={contextValue}>{props.children}</InstanceConfigContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
InstanceConfigProvider,
|
||||||
|
useInstanceConfig,
|
||||||
|
ensureInstanceConfig as ensureInstanceConfigLoaded,
|
||||||
|
getInstanceConfig,
|
||||||
|
updateInstanceConfig,
|
||||||
|
clearInstanceConfig,
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { sseManager } from "../lib/sse-manager"
|
|||||||
import { cliApi } from "../lib/api-client"
|
import { cliApi } from "../lib/api-client"
|
||||||
import { cliEvents } from "../lib/cli-events"
|
import { cliEvents } from "../lib/cli-events"
|
||||||
import type { WorkspaceDescriptor, WorkspaceEventPayload, WorkspaceLogEntry } from "../../../cli/src/api-types"
|
import type { WorkspaceDescriptor, WorkspaceEventPayload, WorkspaceLogEntry } from "../../../cli/src/api-types"
|
||||||
|
import { ensureInstanceConfigLoaded } from "./instance-config"
|
||||||
import {
|
import {
|
||||||
fetchSessions,
|
fetchSessions,
|
||||||
fetchAgents,
|
fetchAgents,
|
||||||
@@ -20,6 +21,7 @@ import { computeDisplayParts } from "./session-messages"
|
|||||||
import { withSession, setSessionPendingPermission } from "./session-state"
|
import { withSession, setSessionPendingPermission } from "./session-state"
|
||||||
import { setHasInstances } from "./ui"
|
import { setHasInstances } from "./ui"
|
||||||
|
|
||||||
|
|
||||||
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
|
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
|
||||||
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
||||||
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
|
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
|
||||||
@@ -116,6 +118,7 @@ async function hydrateInstanceData(instanceId: string) {
|
|||||||
await fetchSessions(instanceId)
|
await fetchSessions(instanceId)
|
||||||
await fetchAgents(instanceId)
|
await fetchAgents(instanceId)
|
||||||
await fetchProviders(instanceId)
|
await fetchProviders(instanceId)
|
||||||
|
await ensureInstanceConfigLoaded(instanceId)
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance?.client) return
|
if (!instance?.client) return
|
||||||
await fetchCommands(instanceId, instance.client)
|
await fetchCommands(instanceId, instance.client)
|
||||||
|
|||||||
@@ -1,88 +1,35 @@
|
|||||||
import type { InstanceData } from "../../../cli/src/api-types"
|
import type { InstanceData } from "../../../cli/src/api-types"
|
||||||
import { storage } from "../lib/storage"
|
import {
|
||||||
|
ensureInstanceConfigLoaded,
|
||||||
|
getInstanceConfig,
|
||||||
|
updateInstanceConfig,
|
||||||
|
} from "./instance-config"
|
||||||
|
|
||||||
const MAX_HISTORY = 100
|
const MAX_HISTORY = 100
|
||||||
|
|
||||||
const instanceDataCache = new Map<string, InstanceData>()
|
|
||||||
const instanceSubscriptions = new Map<string, () => void>()
|
|
||||||
|
|
||||||
export async function addToHistory(instanceId: string, text: string): Promise<void> {
|
export async function addToHistory(instanceId: string, text: string): Promise<void> {
|
||||||
const data = await ensureInstanceData(instanceId)
|
if (!instanceId || !text) return
|
||||||
const nextHistory = [text, ...data.messageHistory]
|
await ensureInstanceConfigLoaded(instanceId)
|
||||||
if (nextHistory.length > MAX_HISTORY) {
|
await updateInstanceConfig(instanceId, (draft) => {
|
||||||
nextHistory.length = MAX_HISTORY
|
const nextHistory = [text, ...(draft.messageHistory ?? [])]
|
||||||
}
|
if (nextHistory.length > MAX_HISTORY) {
|
||||||
|
nextHistory.length = MAX_HISTORY
|
||||||
const nextData: InstanceData = {
|
}
|
||||||
...data,
|
draft.messageHistory = nextHistory
|
||||||
messageHistory: nextHistory,
|
})
|
||||||
}
|
|
||||||
|
|
||||||
instanceDataCache.set(instanceId, cloneInstanceData(nextData))
|
|
||||||
|
|
||||||
try {
|
|
||||||
await storage.saveInstanceData(instanceId, nextData)
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Failed to persist message history:", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHistory(instanceId: string): Promise<string[]> {
|
export async function getHistory(instanceId: string): Promise<string[]> {
|
||||||
const data = await ensureInstanceData(instanceId)
|
if (!instanceId) return []
|
||||||
return [...data.messageHistory]
|
await ensureInstanceConfigLoaded(instanceId)
|
||||||
|
const data = getInstanceConfig(instanceId)
|
||||||
|
return [...(data.messageHistory ?? [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearHistory(instanceId: string): Promise<void> {
|
export async function clearHistory(instanceId: string): Promise<void> {
|
||||||
const data = await ensureInstanceData(instanceId)
|
if (!instanceId) return
|
||||||
const nextData: InstanceData = {
|
await ensureInstanceConfigLoaded(instanceId)
|
||||||
...data,
|
await updateInstanceConfig(instanceId, (draft) => {
|
||||||
messageHistory: [],
|
draft.messageHistory = []
|
||||||
}
|
|
||||||
|
|
||||||
instanceDataCache.set(instanceId, cloneInstanceData(nextData))
|
|
||||||
|
|
||||||
try {
|
|
||||||
await storage.saveInstanceData(instanceId, nextData)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to clear history:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureInstanceData(instanceId: string): Promise<InstanceData> {
|
|
||||||
const cached = instanceDataCache.get(instanceId)
|
|
||||||
if (cached) {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await storage.loadInstanceData(instanceId)
|
|
||||||
const normalized = cloneInstanceData(data)
|
|
||||||
instanceDataCache.set(instanceId, normalized)
|
|
||||||
attachInstanceSubscription(instanceId)
|
|
||||||
return normalized
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to load history:", error)
|
|
||||||
const fallback = cloneInstanceData({ messageHistory: [] })
|
|
||||||
instanceDataCache.set(instanceId, fallback)
|
|
||||||
attachInstanceSubscription(instanceId)
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachInstanceSubscription(instanceId: string) {
|
|
||||||
if (instanceSubscriptions.has(instanceId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const unsubscribe = storage.onInstanceDataChanged(instanceId, (data) => {
|
|
||||||
instanceDataCache.set(instanceId, cloneInstanceData(data))
|
|
||||||
})
|
})
|
||||||
instanceSubscriptions.set(instanceId, unsubscribe)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneInstanceData(data?: InstanceData | null): InstanceData {
|
|
||||||
const source: InstanceData = data ?? { messageHistory: [] }
|
|
||||||
return {
|
|
||||||
...source,
|
|
||||||
messageHistory: Array.isArray(source.messageHistory) ? [...source.messageHistory] : [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
|
import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
|
||||||
import type { Accessor, ParentComponent } from "solid-js"
|
import type { Accessor, ParentComponent } from "solid-js"
|
||||||
import { storage, type ConfigData } from "../lib/storage"
|
import { storage, type ConfigData } from "../lib/storage"
|
||||||
|
import {
|
||||||
|
ensureInstanceConfigLoaded,
|
||||||
|
getInstanceConfig,
|
||||||
|
updateInstanceConfig as updateInstanceData,
|
||||||
|
} from "./instance-config"
|
||||||
|
|
||||||
type DeepReadonly<T> = T extends (...args: any[]) => unknown
|
type DeepReadonly<T> = T extends (...args: any[]) => unknown
|
||||||
? T
|
? T
|
||||||
@@ -27,7 +32,6 @@ export interface Preferences {
|
|||||||
lastUsedBinary?: string
|
lastUsedBinary?: string
|
||||||
environmentVariables: Record<string, string>
|
environmentVariables: Record<string, string>
|
||||||
modelRecents: ModelPreference[]
|
modelRecents: ModelPreference[]
|
||||||
agentModelSelections: AgentModelSelections
|
|
||||||
diffViewMode: DiffViewMode
|
diffViewMode: DiffViewMode
|
||||||
toolOutputExpansion: ExpansionPreference
|
toolOutputExpansion: ExpansionPreference
|
||||||
diagnosticsExpansion: ExpansionPreference
|
diagnosticsExpansion: ExpansionPreference
|
||||||
@@ -53,7 +57,6 @@ const defaultPreferences: Preferences = {
|
|||||||
showThinkingBlocks: false,
|
showThinkingBlocks: false,
|
||||||
environmentVariables: {},
|
environmentVariables: {},
|
||||||
modelRecents: [],
|
modelRecents: [],
|
||||||
agentModelSelections: {},
|
|
||||||
diffViewMode: "split",
|
diffViewMode: "split",
|
||||||
toolOutputExpansion: "expanded",
|
toolOutputExpansion: "expanded",
|
||||||
diagnosticsExpansion: "expanded",
|
diagnosticsExpansion: "expanded",
|
||||||
@@ -71,32 +74,24 @@ function deepEqual(a: unknown, b: unknown): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePreferences(pref?: Partial<Preferences>): Preferences {
|
function normalizePreferences(pref?: Partial<Preferences> & { agentModelSelections?: unknown }): Preferences {
|
||||||
|
const sanitized = pref ?? {}
|
||||||
const environmentVariables = {
|
const environmentVariables = {
|
||||||
...defaultPreferences.environmentVariables,
|
...defaultPreferences.environmentVariables,
|
||||||
...(pref?.environmentVariables ?? {}),
|
...(sanitized.environmentVariables ?? {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceModelRecents = pref?.modelRecents ?? defaultPreferences.modelRecents
|
const sourceModelRecents = sanitized.modelRecents ?? defaultPreferences.modelRecents
|
||||||
const modelRecents = sourceModelRecents.map((item) => ({ ...item }))
|
const modelRecents = sourceModelRecents.map((item) => ({ ...item }))
|
||||||
|
|
||||||
const sourceAgentSelections = pref?.agentModelSelections ?? defaultPreferences.agentModelSelections
|
|
||||||
const agentModelSelections: AgentModelSelections = {}
|
|
||||||
for (const [instanceId, selections] of Object.entries(sourceAgentSelections)) {
|
|
||||||
agentModelSelections[instanceId] = Object.fromEntries(
|
|
||||||
Object.entries(selections).map(([agentId, selection]) => [agentId, { ...selection }]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showThinkingBlocks: pref?.showThinkingBlocks ?? defaultPreferences.showThinkingBlocks,
|
showThinkingBlocks: sanitized.showThinkingBlocks ?? defaultPreferences.showThinkingBlocks,
|
||||||
lastUsedBinary: pref?.lastUsedBinary ?? defaultPreferences.lastUsedBinary,
|
lastUsedBinary: sanitized.lastUsedBinary ?? defaultPreferences.lastUsedBinary,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
modelRecents,
|
modelRecents,
|
||||||
agentModelSelections,
|
diffViewMode: sanitized.diffViewMode ?? defaultPreferences.diffViewMode,
|
||||||
diffViewMode: pref?.diffViewMode ?? defaultPreferences.diffViewMode,
|
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion,
|
||||||
toolOutputExpansion: pref?.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion,
|
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion,
|
||||||
diagnosticsExpansion: pref?.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +117,22 @@ function buildFallbackConfig(): ConfigData {
|
|||||||
return normalizeConfig()
|
return normalizeConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeLegacyAgentSelections(config?: ConfigData | null): { cleaned: ConfigData; migrated: boolean } {
|
||||||
|
const migrated = Boolean((config?.preferences as { agentModelSelections?: unknown } | undefined)?.agentModelSelections)
|
||||||
|
const cleanedConfig = normalizeConfig(config)
|
||||||
|
return { cleaned: cleanedConfig, migrated }
|
||||||
|
}
|
||||||
|
|
||||||
async function syncConfig(source?: ConfigData): Promise<void> {
|
async function syncConfig(source?: ConfigData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const configData = source ?? (await storage.loadConfig())
|
const loaded = source ?? (await storage.loadConfig())
|
||||||
applyConfig(configData)
|
const { cleaned, migrated } = removeLegacyAgentSelections(loaded)
|
||||||
|
applyConfig(cleaned)
|
||||||
|
if (migrated) {
|
||||||
|
void storage.updateConfig(cleaned).catch((error: unknown) => {
|
||||||
|
console.error("Failed to persist legacy config cleanup:", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load config:", error)
|
console.error("Failed to load config:", error)
|
||||||
applyConfig(buildFallbackConfig())
|
applyConfig(buildFallbackConfig())
|
||||||
@@ -328,27 +335,25 @@ function addRecentModelPreference(model: ModelPreference): void {
|
|||||||
updatePreferences({ modelRecents: updated })
|
updatePreferences({ modelRecents: updated })
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAgentModelPreference(instanceId: string, agent: string, model: ModelPreference): void {
|
async function setAgentModelPreference(instanceId: string, agent: string, model: ModelPreference): Promise<void> {
|
||||||
if (!instanceId || !agent || !model.providerId || !model.modelId) return
|
if (!instanceId || !agent || !model.providerId || !model.modelId) return
|
||||||
const selections = preferences().agentModelSelections ?? {}
|
await ensureInstanceConfigLoaded(instanceId)
|
||||||
const instanceSelections = selections[instanceId] ?? {}
|
await updateInstanceData(instanceId, (draft) => {
|
||||||
const existing = instanceSelections[agent]
|
const selections = { ...(draft.agentModelSelections ?? {}) }
|
||||||
if (existing && existing.providerId === model.providerId && existing.modelId === model.modelId) {
|
const existing = selections[agent]
|
||||||
return
|
if (existing && existing.providerId === model.providerId && existing.modelId === model.modelId) {
|
||||||
}
|
return
|
||||||
updatePreferences({
|
}
|
||||||
agentModelSelections: {
|
selections[agent] = model
|
||||||
...selections,
|
draft.agentModelSelections = selections
|
||||||
[instanceId]: {
|
|
||||||
...instanceSelections,
|
|
||||||
[agent]: model,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAgentModelPreference(instanceId: string, agent: string): ModelPreference | undefined {
|
async function getAgentModelPreference(instanceId: string, agent: string): Promise<ModelPreference | undefined> {
|
||||||
return preferences().agentModelSelections?.[instanceId]?.[agent]
|
if (!instanceId || !agent) return undefined
|
||||||
|
await ensureInstanceConfigLoaded(instanceId)
|
||||||
|
const selections = getInstanceConfig(instanceId).agentModelSelections ?? {}
|
||||||
|
return selections[agent]
|
||||||
}
|
}
|
||||||
|
|
||||||
void ensureConfigLoaded().catch((error: unknown) => {
|
void ensureConfigLoaded().catch((error: unknown) => {
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ async function updateSessionAgent(instanceId: string, sessionId: string, agent:
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (agent && shouldApplyModel) {
|
if (agent && shouldApplyModel) {
|
||||||
setAgentModelPreference(instanceId, agent, nextModel)
|
await setAgentModelPreference(instanceId, agent, nextModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldApplyModel) {
|
if (shouldApplyModel) {
|
||||||
@@ -335,7 +335,7 @@ async function updateSessionModel(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (session.agent) {
|
if (session.agent) {
|
||||||
setAgentModelPreference(instanceId, session.agent, model)
|
await setAgentModelPreference(instanceId, session.agent, model)
|
||||||
}
|
}
|
||||||
addRecentModelPreference(model)
|
addRecentModelPreference(model)
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||||||
const defaultModel = await getDefaultModel(instanceId, selectedAgent)
|
const defaultModel = await getDefaultModel(instanceId, selectedAgent)
|
||||||
|
|
||||||
if (selectedAgent && isModelValid(instanceId, defaultModel)) {
|
if (selectedAgent && isModelValid(instanceId, defaultModel)) {
|
||||||
setAgentModelPreference(instanceId, selectedAgent, defaultModel)
|
await setAgentModelPreference(instanceId, selectedAgent, defaultModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading((prev) => {
|
setLoading((prev) => {
|
||||||
|
|||||||
@@ -32,13 +32,6 @@ async function getDefaultModel(
|
|||||||
const instanceProviders = providers().get(instanceId) || []
|
const instanceProviders = providers().get(instanceId) || []
|
||||||
const instanceAgents = agents().get(instanceId) || []
|
const instanceAgents = agents().get(instanceId) || []
|
||||||
|
|
||||||
if (agentName) {
|
|
||||||
const stored = getAgentModelPreference(instanceId, agentName)
|
|
||||||
if (isModelValid(instanceId, stored)) {
|
|
||||||
return stored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agentName) {
|
if (agentName) {
|
||||||
const agent = instanceAgents.find((a) => a.name === agentName)
|
const agent = instanceAgents.find((a) => a.name === agentName)
|
||||||
if (agent && agent.model && isModelValid(instanceId, agent.model)) {
|
if (agent && agent.model && isModelValid(instanceId, agent.model)) {
|
||||||
@@ -47,6 +40,11 @@ async function getDefaultModel(
|
|||||||
modelId: agent.model.modelId,
|
modelId: agent.model.modelId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stored = await getAgentModelPreference(instanceId, agentName)
|
||||||
|
if (isModelValid(instanceId, stored)) {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const recent = getRecentModelPreferenceForInstance(instanceId)
|
const recent = getRecentModelPreferenceForInstance(instanceId)
|
||||||
|
|||||||
Reference in New Issue
Block a user