mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 18:26:09 +00:00
fix(app): default advanced features for new users
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { resolveAgent } from "./local-agent"
|
||||
|
||||
describe("resolveAgent", () => {
|
||||
const agents = [{ name: "plan" }, { name: "build" }, { name: "custom" }]
|
||||
|
||||
test("uses the requested available agent", () => {
|
||||
expect(resolveAgent(agents, "custom")?.name).toBe("custom")
|
||||
})
|
||||
|
||||
test("defaults to build", () => {
|
||||
expect(resolveAgent(agents)?.name).toBe("build")
|
||||
expect(resolveAgent(agents, "missing")?.name).toBe("build")
|
||||
})
|
||||
|
||||
test("uses the first agent when build is unavailable", () => {
|
||||
expect(resolveAgent([{ name: "custom" }], "missing")?.name).toBe("custom")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export function resolveAgent<T extends { name: string }>(items: T[], name?: string) {
|
||||
return items.find((item) => item.name === name) ?? items.find((item) => item.name === "build") ?? items[0]
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import { useParams } from "@solidjs/router"
|
||||
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useModels } from "@/context/models"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { resolveAgent } from "./local-agent"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
@@ -62,6 +64,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const serverSDK = useServerSDK()
|
||||
const providers = useProviders(() => sdk().directory)
|
||||
const models = useModels()
|
||||
const settings = useSettings()
|
||||
|
||||
const id = createMemo(() => params.id || undefined)
|
||||
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
||||
@@ -88,7 +91,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
variant?: string | null
|
||||
}
|
||||
}>({
|
||||
current: list()[0]?.name,
|
||||
current: resolveAgent(list())?.name,
|
||||
draft: undefined,
|
||||
last: undefined,
|
||||
})
|
||||
@@ -107,9 +110,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
const pickAgent = (name: string | undefined) => {
|
||||
const items = list()
|
||||
if (items.length === 0) return
|
||||
return items.find((item) => item.name === name) ?? items[0]
|
||||
return resolveAgent(list(), name)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@@ -181,6 +182,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const agent = {
|
||||
list,
|
||||
current() {
|
||||
if (!settings.visibility.customAgents()) return pickAgent("build")
|
||||
return pickAgent(scope()?.agent ?? store.current)
|
||||
},
|
||||
set(name: string | undefined) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isAppUpgrade,
|
||||
layoutTransitionState,
|
||||
maximumSunsetTimeout,
|
||||
migrateSettings,
|
||||
newLayoutDesignsDefault,
|
||||
nextSunsetCheckDelay,
|
||||
resolveNewLayoutDesigns,
|
||||
@@ -10,6 +11,39 @@ import {
|
||||
shouldEnableNewLayout,
|
||||
} from "./settings"
|
||||
|
||||
describe("feature visibility", () => {
|
||||
test("enables features once for profiles created before the visibility defaults", () => {
|
||||
expect(
|
||||
migrateSettings({
|
||||
general: {
|
||||
showFileTree: false,
|
||||
showSearch: false,
|
||||
showStatus: false,
|
||||
showCustomAgents: false,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
general: {
|
||||
showFileTree: true,
|
||||
showSearch: true,
|
||||
showStatus: true,
|
||||
showCustomAgents: true,
|
||||
featureVisibilityInitialized: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves preferences after the visibility defaults are initialized", () => {
|
||||
const value = {
|
||||
general: {
|
||||
showFileTree: false,
|
||||
featureVisibilityInitialized: true,
|
||||
},
|
||||
}
|
||||
expect(migrateSettings(value)).toBe(value)
|
||||
})
|
||||
})
|
||||
|
||||
describe("layout transition", () => {
|
||||
test("blank profiles default to the new layout", () => {
|
||||
expect(newLayoutDesignsDefault).toBe(true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { persisted } from "@/utils/persist"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
@@ -36,6 +36,7 @@ export interface Settings {
|
||||
mobileTitlebarPosition: "top" | "bottom"
|
||||
newLayoutDesigns?: boolean
|
||||
layoutTransitionEligible?: boolean
|
||||
featureVisibilityInitialized?: boolean
|
||||
newInterfaceNoticeDismissed?: boolean
|
||||
shouldDisplayTabsToast?: boolean
|
||||
}
|
||||
@@ -62,6 +63,27 @@ export const newLayoutDesignsDefault = true
|
||||
export const oldInterfaceSunset = new Date(2026, 8, 14)
|
||||
const newLayoutDesignsUpgradeCutoff = "1.17.19"
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export function migrateSettings(value: unknown) {
|
||||
if (!isRecord(value)) return value
|
||||
const general = isRecord(value.general) ? value.general : {}
|
||||
if (general.featureVisibilityInitialized === true) return value
|
||||
return {
|
||||
...value,
|
||||
general: {
|
||||
...general,
|
||||
showFileTree: true,
|
||||
showSearch: true,
|
||||
showStatus: true,
|
||||
showCustomAgents: true,
|
||||
featureVisibilityInitialized: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function compareVersions(a: string, b: string) {
|
||||
const parse = (version: string) => {
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/i.exec(version.trim())
|
||||
@@ -220,7 +242,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
gate: false,
|
||||
init: () => {
|
||||
const platform = usePlatform()
|
||||
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{ key: "settings.v3", migrate: migrateSettings },
|
||||
createStore<Settings>(defaultSettings),
|
||||
)
|
||||
const [launch, setLaunch, , launchReady] = persisted(
|
||||
"app-version.v1",
|
||||
createStore<{ version?: string }>({ version: undefined }),
|
||||
@@ -266,7 +291,17 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
layoutTransitionEligible() ? legacyNewLayoutDesignsDefault : newLayoutDesignsDefault,
|
||||
)
|
||||
})
|
||||
const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference())
|
||||
|
||||
const initializeFeatureVisibility = (existing: boolean) => {
|
||||
if (store.general?.featureVisibilityInitialized === true) return
|
||||
batch(() => {
|
||||
setStore("general", "showFileTree", existing)
|
||||
setStore("general", "showSearch", existing)
|
||||
setStore("general", "showStatus", existing)
|
||||
setStore("general", "showCustomAgents", existing)
|
||||
setStore("general", "featureVisibilityInitialized", true)
|
||||
})
|
||||
}
|
||||
|
||||
if (sunset && !oldInterfaceRetired()) {
|
||||
const timeout = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||
@@ -318,6 +353,11 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setStore("general", "newLayoutDesigns", true)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready() || platform.platform === "desktop") return
|
||||
initializeFeatureVisibility(false)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
const root = document.documentElement
|
||||
@@ -416,6 +456,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
if (typeof current === "boolean") return
|
||||
setStore("general", "layoutTransitionEligible", eligible)
|
||||
},
|
||||
initializeFeatureVisibility,
|
||||
layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available),
|
||||
newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice),
|
||||
dismissNewInterfaceNotice() {
|
||||
@@ -427,10 +468,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
fileTree: visible(showFileTree),
|
||||
search: visible(showSearch),
|
||||
status: visible(showStatus),
|
||||
customAgents: visible(showCustomAgents),
|
||||
fileTree: showFileTree,
|
||||
search: showSearch,
|
||||
status: showStatus,
|
||||
customAgents: showCustomAgents,
|
||||
},
|
||||
appearance: {
|
||||
fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
|
||||
|
||||
@@ -17,6 +17,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
|
||||
)
|
||||
const existingInstall = await window.api.isOldLayoutEligible()
|
||||
settings.general.setOldLayoutEligible(existingInstall)
|
||||
settings.general.initializeFeatureVisibility(existingInstall)
|
||||
if (!server.isLocal()) return
|
||||
|
||||
const pending = await window.api.isFirstLaunchOnboardingPending()
|
||||
|
||||
Reference in New Issue
Block a user