mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 02:49:57 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0e4870730 |
@@ -1,6 +1,6 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
@@ -11,21 +11,22 @@ import { useSDK } from "./sdk"
|
|||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import { useServerSDK } from "./server-sdk"
|
||||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||||
|
import {
|
||||||
|
cloneModelSelectionState,
|
||||||
|
createModelSelectionState,
|
||||||
|
createTabModelSelectionState,
|
||||||
|
type ModelSelectionState,
|
||||||
|
} from "./model-selection-state"
|
||||||
|
import { useTabs } from "./tabs"
|
||||||
|
|
||||||
export type ModelKey = { providerID: string; modelID: string; variant?: string }
|
export type ModelKey = { providerID: string; modelID: string; variant?: string }
|
||||||
|
|
||||||
type State = {
|
|
||||||
agent?: string
|
|
||||||
model?: ModelKey
|
|
||||||
variant?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type Saved = {
|
type Saved = {
|
||||||
session: Record<string, State | undefined>
|
session: Record<string, ModelSelectionState | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_KEY = "__workspace__"
|
const WORKSPACE_KEY = "__workspace__"
|
||||||
const handoff = new Map<string, State>()
|
const handoff = new Map<string, ModelSelectionState>()
|
||||||
|
|
||||||
const handoffKey = (scope: ServerScope, dir: string, id: string) => ScopedKey.from(scope, dir, id)
|
const handoffKey = (scope: ServerScope, dir: string, id: string) => ScopedKey.from(scope, dir, id)
|
||||||
|
|
||||||
@@ -33,8 +34,8 @@ const migrate = (value: unknown) => {
|
|||||||
if (!value || typeof value !== "object") return { session: {} }
|
if (!value || typeof value !== "object") return { session: {} }
|
||||||
|
|
||||||
const item = value as {
|
const item = value as {
|
||||||
session?: Record<string, State | undefined>
|
session?: Record<string, ModelSelectionState | undefined>
|
||||||
pick?: Record<string, State | undefined>
|
pick?: Record<string, ModelSelectionState | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.session && typeof item.session === "object") return { session: item.session }
|
if (item.session && typeof item.session === "object") return { session: item.session }
|
||||||
@@ -45,14 +46,6 @@ const migrate = (value: unknown) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clone = (value: State | undefined) => {
|
|
||||||
if (!value) return
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
model: value.model ? { ...value.model } : undefined,
|
|
||||||
} satisfies State
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -62,6 +55,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const providers = useProviders(() => sdk().directory)
|
const providers = useProviders(() => sdk().directory)
|
||||||
const models = useModels()
|
const models = useModels()
|
||||||
|
const tabs = useTabs()
|
||||||
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
|
|
||||||
const id = createMemo(() => params.id || undefined)
|
const id = createMemo(() => params.id || undefined)
|
||||||
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
||||||
@@ -79,7 +74,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
current?: string
|
current?: string
|
||||||
draft?: State
|
draft?: ModelSelectionState
|
||||||
last?: {
|
last?: {
|
||||||
type: "agent" | "model" | "variant"
|
type: "agent" | "model" | "variant"
|
||||||
agent?: string
|
agent?: string
|
||||||
@@ -121,10 +116,35 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
setStore("current", items[0]?.name)
|
setStore("current", items[0]?.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
const scope = createMemo<State | undefined>(() => {
|
const draft = createMemo(() => {
|
||||||
|
const draftID = search.draftId
|
||||||
|
if (!draftID) return
|
||||||
|
const tab = tabs.store.find((item) => item.type === "draft" && item.draftID === draftID)
|
||||||
|
if (!tab || tab.type !== "draft") return
|
||||||
|
return createTabModelSelectionState(tabs, tab, () => {
|
||||||
|
const [state, setState] = persisted(
|
||||||
|
Persist.draft(tab.draftID, "model-selection"),
|
||||||
|
createStore<{ value?: ModelSelectionState }>({}),
|
||||||
|
)
|
||||||
|
return createModelSelectionState([state, setState])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const setDraft = (value: ModelSelectionState | undefined) => {
|
||||||
|
const state = draft()
|
||||||
|
if (state) {
|
||||||
|
state.set(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStore("draft", cloneModelSelectionState(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = createMemo<ModelSelectionState | undefined>(() => {
|
||||||
const session = id()
|
const session = id()
|
||||||
if (!session) return store.draft
|
if (session) return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
|
||||||
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
|
const state = draft()
|
||||||
|
if (state) return state.current()
|
||||||
|
return store.draft
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -139,7 +159,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaved("session", session, clone(next))
|
setSaved("session", session, cloneModelSelectionState(next))
|
||||||
handoff.delete(key)
|
handoff.delete(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -200,13 +220,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
agent: item.name,
|
agent: item.name,
|
||||||
model: item.model ?? prev?.model,
|
model: item.model ?? prev?.model,
|
||||||
variant: item.variant ?? prev?.variant,
|
variant: item.variant ?? prev?.variant,
|
||||||
} satisfies State
|
} satisfies ModelSelectionState
|
||||||
const session = id()
|
const session = id()
|
||||||
if (session) {
|
if (session) {
|
||||||
setSaved("session", session, next)
|
setSaved("session", session, next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("draft", next)
|
setDraft(next)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
move(direction: 1 | -1) {
|
move(direction: 1 | -1) {
|
||||||
@@ -253,21 +273,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
agent: agent.current()?.name,
|
agent: agent.current()?.name,
|
||||||
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
|
||||||
variant: selected(),
|
variant: selected(),
|
||||||
} satisfies State
|
} satisfies ModelSelectionState
|
||||||
}
|
}
|
||||||
|
|
||||||
const write = (next: Partial<State>) => {
|
const write = (next: Partial<ModelSelectionState>) => {
|
||||||
const state = {
|
const state = {
|
||||||
...(scope() ?? { agent: agent.current()?.name }),
|
...(scope() ?? { agent: agent.current()?.name }),
|
||||||
...next,
|
...next,
|
||||||
} satisfies State
|
} satisfies ModelSelectionState
|
||||||
|
|
||||||
const session = id()
|
const session = id()
|
||||||
if (session) {
|
if (session) {
|
||||||
setSaved("session", session, state)
|
setSaved("session", session, state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("draft", state)
|
setDraft(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
|
||||||
@@ -369,20 +389,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
agent,
|
agent,
|
||||||
session: {
|
session: {
|
||||||
reset() {
|
reset() {
|
||||||
setStore("draft", undefined)
|
setDraft(undefined)
|
||||||
},
|
},
|
||||||
promote(dir: string, session: string) {
|
promote(dir: string, session: string) {
|
||||||
const next = clone(snapshot())
|
const next = cloneModelSelectionState(snapshot())
|
||||||
if (!next) return
|
if (!next) return
|
||||||
|
|
||||||
if (dir === sdk().directory) {
|
if (dir === sdk().directory) {
|
||||||
setSaved("session", session, next)
|
setSaved("session", session, next)
|
||||||
setStore("draft", undefined)
|
setDraft(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handoff.set(handoffKey(serverSDK().scope, dir, session), next)
|
handoff.set(handoffKey(serverSDK().scope, dir, session), next)
|
||||||
setStore("draft", undefined)
|
setDraft(undefined)
|
||||||
},
|
},
|
||||||
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
||||||
const session = id()
|
const session = id()
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { createRoot, getOwner } from "solid-js"
|
||||||
|
import { createTabMemory } from "./tab-memory"
|
||||||
|
import { createTabModelSelectionState } from "./model-selection-state"
|
||||||
|
import type { DraftTab } from "./tabs"
|
||||||
|
|
||||||
|
describe("model selection state", () => {
|
||||||
|
test("keeps model selection scoped to its draft tab", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const memory = createTabMemory(getOwner())
|
||||||
|
const tabs = {
|
||||||
|
state<T>(tab: DraftTab, name: string, init: () => T) {
|
||||||
|
return memory.ensure(`draft:${tab.draftID}`, name, init)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const first = { type: "draft", draftID: "first", server: "server", directory: "/repo" } as DraftTab
|
||||||
|
const second = { type: "draft", draftID: "second", server: "server", directory: "/repo" } as DraftTab
|
||||||
|
|
||||||
|
createTabModelSelectionState(tabs, first).set({
|
||||||
|
agent: "build",
|
||||||
|
model: { providerID: "anthropic", modelID: "claude" },
|
||||||
|
})
|
||||||
|
createTabModelSelectionState(tabs, second).set({
|
||||||
|
agent: "build",
|
||||||
|
model: { providerID: "openai", modelID: "gpt" },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createTabModelSelectionState(tabs, first).current()?.model).toEqual({
|
||||||
|
providerID: "anthropic",
|
||||||
|
modelID: "claude",
|
||||||
|
})
|
||||||
|
expect(createTabModelSelectionState(tabs, second).current()?.model).toEqual({
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt",
|
||||||
|
})
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
|
import type { DraftTab, useTabs } from "./tabs"
|
||||||
|
|
||||||
|
export type ModelSelectionState = {
|
||||||
|
agent?: string
|
||||||
|
model?: { providerID: string; modelID: string; variant?: string }
|
||||||
|
variant?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneModelSelectionState(value: ModelSelectionState | undefined) {
|
||||||
|
if (!value) return
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
model: value.model ? { ...value.model } : undefined,
|
||||||
|
} satisfies ModelSelectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelSelectionStore = { value?: ModelSelectionState }
|
||||||
|
|
||||||
|
export function createModelSelectionState(
|
||||||
|
state: [Store<ModelSelectionStore>, SetStoreFunction<ModelSelectionStore>] = createStore<ModelSelectionStore>({}),
|
||||||
|
) {
|
||||||
|
const [store, setStore] = state
|
||||||
|
return {
|
||||||
|
current: () => store.value,
|
||||||
|
set(value: ModelSelectionState | undefined) {
|
||||||
|
setStore("value", cloneModelSelectionState(value))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTabModelSelectionState(
|
||||||
|
tabs: Pick<ReturnType<typeof useTabs>, "state">,
|
||||||
|
tab: DraftTab,
|
||||||
|
init: () => ReturnType<typeof createModelSelectionState> = createModelSelectionState,
|
||||||
|
) {
|
||||||
|
return tabs.state(tab, "model-selection", init)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user