There were no user-facing settings. Add vibeThemer.applyTo (global | workspace, default global). writePreference now takes the preferred target and orders the first-success-wins list accordingly — workspace only matters with a folder open, so it falls through to global otherwise. ConfigStore reads the setting via applyTo().
The contributes.configuration block.
package.json · 113 lines package.json 113 lines · JSON expand all
⋯ 71 lines hidden (lines 1–71) 3 "displayName" : "Vibe Themer" , 4 "description" : "🎨 AI-powered VS Code theme generator. Describe your vibe and watch a complete theme stream in live, generated by OpenAI or Anthropic models." , 15 "url" : "https://github.com/mseeks/vibe-themer" 17 "homepage" : "https://github.com/mseeks/vibe-themer#readme" , 19 "url" : "https://github.com/mseeks/vibe-themer/issues" 22 "url" : "https://github.com/sponsors/mseeks" 28 "onCommand:vibeThemer.changeTheme" , 29 "onCommand:vibeThemer.clearApiKey" , 30 "onCommand:vibeThemer.resetTheme" , 31 "onCommand:vibeThemer.selectModel" , 32 "onCommand:vibeThemer.resetModel" 48 "main" : "./out/extension.js" , 52 "command" : "vibeThemer.changeTheme" , 53 "title" : "Vibe Themer: Change Theme" 56 "command" : "vibeThemer.clearApiKey" , 57 "title" : "Vibe Themer: Clear API Keys" 60 "command" : "vibeThemer.resetTheme" , 61 "title" : "Vibe Themer: Reset Theme Customizations" 64 "command" : "vibeThemer.selectModel" , 65 "title" : "Vibe Themer: Select Model" 68 "command" : "vibeThemer.resetModel" , 69 "title" : "Vibe Themer: Reset Model Selection" 73 "title" : "Vibe Themer" , 75 "vibeThemer.applyTo" : { 82 "User (global) settings — the theme follows you across all windows." , 83 "The current workspace's settings — falls back to global if no folder is open." 86 "description" : "Where Vibe Themer writes generated theme customizations." ⋯ 25 lines hidden (lines 89–113) 92 "vscode:prepublish" : "npm run package" , 93 "package" : "npm run check-types && npm run lint && node ./esbuild.js --production" , 94 "compile" : "npm run check-types && npm run lint && node ./esbuild.js" , 95 "check-types" : "tsc --noEmit" , 96 "watch" : "npm run check-types && node ./esbuild.js --watch" , 97 "lint" : "eslint src test --ext ts" , 98 "test" : "node ./esbuild.test.js && node out/test.cjs" 101 "@types/node" : "^20.17.58" , 102 "@types/vscode" : "^1.74.0" , 103 "@typescript-eslint/eslint-plugin" : "^7.7.1" , 104 "@typescript-eslint/parser" : "^7.7.1" , 105 "esbuild" : "^0.25.12" , 107 "typescript" : "^5.4.5" 110 "@anthropic-ai/sdk" : "^0.102.0" ,
Pure ordering: preference first, workspace gated on an open folder.
src/domain/scope.ts · 40 lines src/domain/scope.ts 40 lines · TypeScript expand all
⋯ 20 lines hidden (lines 1–20) 2 * Where theme customizations live. `ConfigurationScope` describes *where existing* 3 * customizations were found; `writePreference` describes where new ones go. 5 * The write preference is an ordered, non-empty list: try the first target, fall 6 * back to the next only if it fails (first-success-wins). The user's `applyTo` 7 * setting picks which target to try first; `workspace` is only meaningful with a 8 * folder open, so without one we always fall through to `global`. 11 import { matchTag , type NonEmptyArray } from '../fp' ; 13 export type WriteTarget = 'global' | 'workspace' ; 15 export type ConfigurationScope = 16 | { readonly _tag : 'Global' } 17 | { readonly _tag : 'Workspace' } 18 | { readonly _tag : 'Both' } 19 | { readonly _tag : 'None' } ; 21 export const writePreference = ( 22 preferred : WriteTarget , 23 hasWorkspaceFolders : boolean , 24 ) : NonEmptyArray < WriteTarget > => { 25 if ( preferred === 'workspace' ) { 26 return hasWorkspaceFolders ? [ 'workspace' , 'global' ] : [ 'global' ] ; 28 return hasWorkspaceFolders ? [ 'global' , 'workspace' ] : [ 'global' ] ; ⋯ 10 lines hidden (lines 31–40) 31 export const describeTarget = ( target : WriteTarget ) : string => 32 target === 'global' ? 'global settings' : 'workspace settings' ; 34 export const describeScope = ( scope : ConfigurationScope ) : string => 36 Global : ( ) => 'global settings' , 37 Workspace : ( ) => 'workspace settings' , 38 Both : ( ) => 'global and workspace settings' , 39 None : ( ) => 'no settings' ,
The adapter reads vibeThemer.applyTo.
src/adapters/vscode/config.ts · 110 lines src/adapters/vscode/config.ts 110 lines · TypeScript expand all
⋯ 77 lines hidden (lines 1–77) 1 import * as vscode from 'vscode' ; 2 import { type AsyncResultType , err , type NonEmptyArray , ok } from '../../fp' ; 3 import { applyColor , applyTokenRule , type TextMateRule } from '../../domain/customizations' ; 4 import { type WriteTarget } from '../../domain/scope' ; 10 type TokenCustomizations , 11 } from '../../domain/theme' ; 12 import { type ConfigError , type ConfigStore } from '../../ports' ; 14 const COLOR_KEY = 'workbench.colorCustomizations' ; 15 const TOKEN_KEY = 'editor.tokenColorCustomizations' ; 17 interface TokenCustomizationsShape { 18 readonly textMateRules? : ReadonlyArray < TextMateRule > ; 19 readonly [ key : string ] : unknown ; 22 // VS Code config is user-editable JSON, so reads are validated, never trusted blindly. 23 const isPlainObject = ( value : unknown ) : value is Record < string , unknown > => 24 typeof value === 'object' && value !== null && ! Array . isArray ( value ) ; 26 const asColorMap = ( value : unknown ) : ColorMap => ( isPlainObject ( value ) ? ( value as ColorMap ) : { } ) ; 28 const asTokenShape = ( value : unknown ) : TokenCustomizationsShape => 29 isPlainObject ( value ) ? ( value as TokenCustomizationsShape ) : { } ; 31 const targetOf = ( target : WriteTarget ) : vscode . ConfigurationTarget => 33 ? vscode.ConfigurationTarget.Global 34 : vscode.ConfigurationTarget.Workspace ; 36 const scopedTheme = ( colors : unknown , tokens : unknown ) : ScopedTheme => ( { 37 colors : asColorMap ( colors ) , 38 tokens : isPlainObject ( tokens ) ? ( tokens as TokenCustomizations ) : { } , 41 const applyToTarget = async ( 42 setting : ThemeSetting , 44 ) : AsyncResultType < ConfigError , void > => { 46 const config = vscode . workspace . getConfiguration ( ) ; 47 const vsTarget = targetOf ( target ) ; 49 if ( setting . _tag === 'SelectorSetting' ) { 50 const existing = asColorMap ( config . get < unknown > ( COLOR_KEY ) ) ; 51 const next = applyColor ( existing , setting . selector , setting . color ) ; 52 await config . update ( COLOR_KEY , next , vsTarget ) ; 54 const existing = asTokenShape ( config . get < unknown > ( TOKEN_KEY ) ) ; 55 const rules = Array . isArray ( existing . textMateRules ) ? existing . textMateRules : [ ] ; 56 const nextRules = applyTokenRule ( rules , setting . scope , setting . color , setting . fontStyle ) ; 57 await config . update ( TOKEN_KEY , { ... existing , textMateRules : nextRules } , vsTarget ) ; 61 return err ( { _tag : 'WriteFailed' , target } ) ; 65 export const createConfigStore = ( ) : ConfigStore => ( { 66 readCurrentTheme : ( ) : CurrentTheme => { 67 const config = vscode . workspace . getConfiguration ( ) ; 68 const colors = config . inspect < unknown > ( COLOR_KEY ) ; 69 const tokens = config . inspect < unknown > ( TOKEN_KEY ) ; 71 global : scopedTheme ( colors ? . globalValue , tokens ? . globalValue ) , 72 workspace : scopedTheme ( colors ? . workspaceValue , tokens ? . workspaceValue ) , 76 hasWorkspaceFolders : ( ) : boolean => ( vscode . workspace . workspaceFolders ? . length ?? 0 ) > 0 , 78 applyTo : ( ) : WriteTarget => 79 vscode . workspace . getConfiguration ( 'vibeThemer' ) . get < string > ( 'applyTo' ) === 'workspace' ⋯ 29 lines hidden (lines 82–110) 84 setting : ThemeSetting , 85 preference : NonEmptyArray < WriteTarget > , 86 ) : AsyncResultType < ConfigError , WriteTarget > => { 87 for ( const target of preference ) { 88 const result = await applyToTarget ( setting , target ) ; 89 if ( result . _tag === 'Ok' ) { 93 return err ( { _tag : 'AllTargetsFailed' } ) ; 96 reset : async ( ) : AsyncResultType < ConfigError , void > => { 97 const config = vscode . workspace . getConfiguration ( ) ; 98 // `allSettled` never rejects, so inspect the outcomes: surface failure only when 99 // every target failed (a workspace-target failure with no folder open is normal). 100 const outcomes = await Promise . allSettled ( [ 101 config . update ( COLOR_KEY , undefined , vscode . ConfigurationTarget . Workspace ) , 102 config . update ( TOKEN_KEY , undefined , vscode . ConfigurationTarget . Workspace ) , 103 config . update ( COLOR_KEY , undefined , vscode . ConfigurationTarget . Global ) , 104 config . update ( TOKEN_KEY , undefined , vscode . ConfigurationTarget . Global ) , 106 return outcomes . every ( ( o ) => o . status === 'rejected' ) 107 ? err ( { _tag : 'AllTargetsFailed' } )