@@ -2,6 +2,7 @@ import { createStore, reconcile } from "solid-js/store"
22import { createEffect , createMemo , createSignal , onCleanup } from "solid-js"
33import { createSimpleContext } from "@opencode-ai/ui/context"
44import { persisted } from "@/utils/persist"
5+ import { usePlatform } from "@/context/platform"
56
67export interface NotificationSettings {
78 agent : boolean
@@ -36,6 +37,7 @@ export interface Settings {
3637 newLayoutDesigns ?: boolean
3738 layoutTransitionEligible ?: boolean
3839 newInterfaceNoticeDismissed ?: boolean
40+ shouldDisplayTabsToast ?: boolean
3941 }
4042 appearance : {
4143 fontSize : number
@@ -57,7 +59,49 @@ export const terminalDefault = "JetBrainsMono Nerd Font Mono"
5759const legacyNewLayoutDesignsDefault = import . meta. env . VITE_OPENCODE_CHANNEL !== "prod"
5860export const newLayoutDesignsDefault = true
5961// Existing users can switch layouts until local midnight on this date. Set new Date(YYYY, M-1, D) to show.
60- export const oldInterfaceSunset = import . meta. env . VITE_OPENCODE_CHANNEL !== "prod" ? new Date ( 2026 , 7 , 14 ) : null
62+ export const oldInterfaceSunset = new Date ( 2026 , 8 , 14 )
63+ const newLayoutDesignsUpgradeCutoff = "1.17.19"
64+
65+ function compareVersions ( a : string , b : string ) {
66+ const parse = ( version : string ) => {
67+ const match = / ^ v ? ( \d + ) \. ( \d + ) \. ( \d + ) (?: [ - + ] .* ) ? $ / i. exec ( version . trim ( ) )
68+ if ( ! match ) return
69+ return match . slice ( 1 ) . map ( Number )
70+ }
71+ const left = parse ( a )
72+ const right = parse ( b )
73+ if ( ! left || ! right ) return
74+ const index = left . findIndex ( ( part , index ) => part !== right [ index ] )
75+ return index === - 1 ? 0 : left [ index ] ! - right [ index ] !
76+ }
77+
78+ export function isAppUpgrade ( previous : string | undefined , current : string | undefined ) {
79+ if ( ! previous || ! current ) return false
80+ const comparison = compareVersions ( current , previous )
81+ return comparison !== undefined && comparison > 0
82+ }
83+
84+ export function shouldDisplayTabsToast (
85+ previous : string | undefined ,
86+ current : string | undefined ,
87+ existingInstall : boolean ,
88+ ) {
89+ return isAppUpgrade ( previous , current ) || ( ! previous && existingInstall )
90+ }
91+
92+ export function shouldEnableNewLayout ( previous : string | undefined , current : string | undefined ) {
93+ if ( ! current ) return false
94+ const currentComparison = compareVersions ( current , newLayoutDesignsUpgradeCutoff )
95+ if ( ! previous ) return currentComparison !== undefined && currentComparison > 0
96+ if ( ! isAppUpgrade ( previous , current ) ) return false
97+ const previousComparison = compareVersions ( previous , newLayoutDesignsUpgradeCutoff )
98+ return (
99+ previousComparison !== undefined &&
100+ currentComparison !== undefined &&
101+ previousComparison <= 0 &&
102+ currentComparison > 0
103+ )
104+ }
61105
62106export function layoutTransitionState ( scheduled : boolean , eligible : boolean , retired : boolean , dismissed : boolean ) {
63107 return {
@@ -175,7 +219,17 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
175219 name : "Settings" ,
176220 gate : false ,
177221 init : ( ) => {
222+ const platform = usePlatform ( )
178223 const [ store , setStore , _ , ready ] = persisted ( "settings.v3" , createStore < Settings > ( defaultSettings ) )
224+ const [ launch , setLaunch , , launchReady ] = persisted (
225+ "app-version.v1" ,
226+ createStore < { version ?: string } > ( { version : undefined } ) ,
227+ )
228+ const [ launchState , setLaunchState ] = createStore ( {
229+ classified : false ,
230+ migrationApplied : false ,
231+ previous : undefined as string | undefined ,
232+ } )
179233 const showFileTree = withFallback ( ( ) => store . general ?. showFileTree , defaultSettings . general . showFileTree )
180234 const showSearch = withFallback ( ( ) => store . general ?. showSearch , defaultSettings . general . showSearch )
181235 const showStatus = withFallback ( ( ) => store . general ?. showStatus , defaultSettings . general . showStatus )
@@ -188,10 +242,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
188242 const layoutTransitionClassified = createMemo ( ( ) => typeof store . general ?. layoutTransitionEligible === "boolean" )
189243 const layoutTransitionEligible = withFallback ( ( ) => store . general ?. layoutTransitionEligible , false )
190244 const newInterfaceNoticeDismissed = withFallback ( ( ) => store . general ?. newInterfaceNoticeDismissed , false )
245+ const layoutUpgrade = createMemo ( ( ) =>
246+ launchState . classified && ! launchState . migrationApplied
247+ ? shouldEnableNewLayout ( launchState . previous , platform . version )
248+ : false ,
249+ )
191250 const layoutTransition = createMemo ( ( ) =>
192251 layoutTransitionState ( ! ! sunset , layoutTransitionEligible ( ) , oldInterfaceRetired ( ) , newInterfaceNoticeDismissed ( ) ) ,
193252 )
194253 const newLayoutDesigns = createMemo ( ( ) => {
254+ if ( layoutUpgrade ( ) ) return true
195255 if ( ! ready ( ) && ! oldInterfaceRetired ( ) ) return legacyNewLayoutDesignsDefault
196256 if ( ! layoutTransitionClassified ( ) ) {
197257 return resolveNewLayoutDesigns (
@@ -223,6 +283,35 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
223283 } )
224284 }
225285
286+ createEffect ( ( ) => {
287+ if ( ! launchReady ( ) || launchState . classified ) return
288+ setLaunchState ( {
289+ classified : true ,
290+ previous : launch . version ,
291+ } )
292+ if ( ! platform . version || launch . version === platform . version ) return
293+ setLaunch ( "version" , platform . version )
294+ } )
295+
296+ createEffect ( ( ) => {
297+ if ( ! ready ( ) || ! launchState . classified || launchState . migrationApplied ) return
298+ if ( layoutUpgrade ( ) && store . general ?. newLayoutDesigns !== true ) {
299+ setStore ( "general" , "newLayoutDesigns" , true )
300+ }
301+ setLaunchState ( "migrationApplied" , true )
302+ } )
303+
304+ createEffect ( ( ) => {
305+ if ( ! ready ( ) || ! launchState . classified ) return
306+ if ( typeof store . general ?. shouldDisplayTabsToast === "boolean" ) return
307+ if ( ! launchState . previous && ! layoutTransitionClassified ( ) ) return
308+ setStore (
309+ "general" ,
310+ "shouldDisplayTabsToast" ,
311+ shouldDisplayTabsToast ( launchState . previous , platform . version , layoutTransitionEligible ( ) ) ,
312+ )
313+ } )
314+
226315 createEffect ( ( ) => {
227316 if ( ! ready ( ) || ! oldInterfaceRetired ( ) ) return
228317 if ( store . general ?. newLayoutDesigns === true ) return
@@ -316,7 +405,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
316405 } ,
317406 newLayoutDesigns,
318407 setNewLayoutDesigns ( value : boolean ) {
319- setStore ( "general" , "newLayoutDesigns" , oldInterfaceRetired ( ) ? true : value )
408+ const next = oldInterfaceRetired ( ) ? true : value
409+ if ( newLayoutDesigns ( ) === next ) return
410+ setStore ( "general" , "newLayoutDesigns" , next )
411+ if ( typeof window !== "undefined" ) setTimeout ( ( ) => window . location . reload ( ) )
320412 } ,
321413 layoutTransitionClassified,
322414 setOldLayoutEligible ( eligible : boolean ) {
@@ -329,6 +421,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
329421 dismissNewInterfaceNotice ( ) {
330422 setStore ( "general" , "newInterfaceNoticeDismissed" , true )
331423 } ,
424+ shouldDisplayTabsToast : withFallback ( ( ) => store . general ?. shouldDisplayTabsToast , false ) ,
425+ dismissTabsToast ( ) {
426+ setStore ( "general" , "shouldDisplayTabsToast" , false )
427+ } ,
332428 } ,
333429 visibility : {
334430 fileTree : visible ( showFileTree ) ,
0 commit comments