settings.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { createStore, reconcile } from "solid-js/store"
  2. import { createEffect, createMemo } from "solid-js"
  3. import { createSimpleContext } from "@opencode-ai/ui/context"
  4. import { persisted } from "@/utils/persist"
  5. export interface NotificationSettings {
  6. agent: boolean
  7. permissions: boolean
  8. errors: boolean
  9. }
  10. export interface SoundSettings {
  11. agentEnabled: boolean
  12. agent: string
  13. permissionsEnabled: boolean
  14. permissions: string
  15. errorsEnabled: boolean
  16. errors: string
  17. }
  18. export interface Settings {
  19. general: {
  20. autoSave: boolean
  21. releaseNotes: boolean
  22. followup: "queue" | "steer"
  23. showReasoningSummaries: boolean
  24. shellToolPartsExpanded: boolean
  25. editToolPartsExpanded: boolean
  26. }
  27. updates: {
  28. startup: boolean
  29. }
  30. appearance: {
  31. fontSize: number
  32. font: string
  33. uiFont: string
  34. }
  35. keybinds: Record<string, string>
  36. permissions: {
  37. autoApprove: boolean
  38. }
  39. notifications: NotificationSettings
  40. sounds: SoundSettings
  41. }
  42. export const monoDefault = "IBM Plex Mono"
  43. export const sansDefault = "Inter"
  44. const monoFallback =
  45. 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
  46. const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
  47. const monoBase = `"${monoDefault}", "IBM Plex Mono Fallback", ${monoFallback}`
  48. const sansBase = `"${sansDefault}", "Inter Fallback", ${sansFallback}`
  49. const monoKey = "ibm-plex-mono"
  50. function input(font: string | undefined, key?: string) {
  51. if (!font || font === key || !font.trim()) return ""
  52. return font
  53. }
  54. function family(font: string) {
  55. if (/^[\w-]+$/.test(font)) return font
  56. return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
  57. }
  58. function stack(font: string | undefined, base: string, key?: string) {
  59. const value = input(font, key).trim()
  60. if (!value) return base
  61. return `${family(value)}, ${base}`
  62. }
  63. export function monoInput(font: string | undefined) {
  64. return input(font, monoKey)
  65. }
  66. export function sansInput(font: string | undefined) {
  67. return input(font)
  68. }
  69. export function monoFontFamily(font: string | undefined) {
  70. return stack(font, monoBase, monoKey)
  71. }
  72. export function sansFontFamily(font: string | undefined) {
  73. return stack(font, sansBase)
  74. }
  75. const defaultSettings: Settings = {
  76. general: {
  77. autoSave: true,
  78. releaseNotes: true,
  79. followup: "steer",
  80. showReasoningSummaries: false,
  81. shellToolPartsExpanded: false,
  82. editToolPartsExpanded: false,
  83. },
  84. updates: {
  85. startup: true,
  86. },
  87. appearance: {
  88. fontSize: 14,
  89. font: "",
  90. uiFont: "",
  91. },
  92. keybinds: {},
  93. permissions: {
  94. autoApprove: false,
  95. },
  96. notifications: {
  97. agent: true,
  98. permissions: true,
  99. errors: false,
  100. },
  101. sounds: {
  102. agentEnabled: true,
  103. agent: "staplebops-01",
  104. permissionsEnabled: true,
  105. permissions: "staplebops-02",
  106. errorsEnabled: true,
  107. errors: "nope-03",
  108. },
  109. }
  110. function withFallback<T>(read: () => T | undefined, fallback: T) {
  111. return createMemo(() => read() ?? fallback)
  112. }
  113. export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
  114. name: "Settings",
  115. init: () => {
  116. const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
  117. createEffect(() => {
  118. if (typeof document === "undefined") return
  119. const root = document.documentElement
  120. root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
  121. root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.uiFont))
  122. })
  123. return {
  124. ready,
  125. get current() {
  126. return store
  127. },
  128. general: {
  129. autoSave: withFallback(() => store.general?.autoSave, defaultSettings.general.autoSave),
  130. setAutoSave(value: boolean) {
  131. setStore("general", "autoSave", value)
  132. },
  133. releaseNotes: withFallback(() => store.general?.releaseNotes, defaultSettings.general.releaseNotes),
  134. setReleaseNotes(value: boolean) {
  135. setStore("general", "releaseNotes", value)
  136. },
  137. followup: withFallback(() => store.general?.followup, defaultSettings.general.followup),
  138. setFollowup(value: "queue" | "steer") {
  139. setStore("general", "followup", value)
  140. },
  141. showReasoningSummaries: withFallback(
  142. () => store.general?.showReasoningSummaries,
  143. defaultSettings.general.showReasoningSummaries,
  144. ),
  145. setShowReasoningSummaries(value: boolean) {
  146. setStore("general", "showReasoningSummaries", value)
  147. },
  148. shellToolPartsExpanded: withFallback(
  149. () => store.general?.shellToolPartsExpanded,
  150. defaultSettings.general.shellToolPartsExpanded,
  151. ),
  152. setShellToolPartsExpanded(value: boolean) {
  153. setStore("general", "shellToolPartsExpanded", value)
  154. },
  155. editToolPartsExpanded: withFallback(
  156. () => store.general?.editToolPartsExpanded,
  157. defaultSettings.general.editToolPartsExpanded,
  158. ),
  159. setEditToolPartsExpanded(value: boolean) {
  160. setStore("general", "editToolPartsExpanded", value)
  161. },
  162. },
  163. updates: {
  164. startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
  165. setStartup(value: boolean) {
  166. setStore("updates", "startup", value)
  167. },
  168. },
  169. appearance: {
  170. fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
  171. setFontSize(value: number) {
  172. setStore("appearance", "fontSize", value)
  173. },
  174. font: withFallback(() => store.appearance?.font, defaultSettings.appearance.font),
  175. setFont(value: string) {
  176. setStore("appearance", "font", value.trim() ? value : "")
  177. },
  178. uiFont: withFallback(() => store.appearance?.uiFont, defaultSettings.appearance.uiFont),
  179. setUIFont(value: string) {
  180. setStore("appearance", "uiFont", value.trim() ? value : "")
  181. },
  182. },
  183. keybinds: {
  184. get: (action: string) => store.keybinds?.[action],
  185. set(action: string, keybind: string) {
  186. setStore("keybinds", action, keybind)
  187. },
  188. reset(action: string) {
  189. setStore("keybinds", (current) => {
  190. if (!Object.prototype.hasOwnProperty.call(current, action)) return current
  191. const next = { ...current }
  192. delete next[action]
  193. return next
  194. })
  195. },
  196. resetAll() {
  197. setStore("keybinds", reconcile({}))
  198. },
  199. },
  200. permissions: {
  201. autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove),
  202. setAutoApprove(value: boolean) {
  203. setStore("permissions", "autoApprove", value)
  204. },
  205. },
  206. notifications: {
  207. agent: withFallback(() => store.notifications?.agent, defaultSettings.notifications.agent),
  208. setAgent(value: boolean) {
  209. setStore("notifications", "agent", value)
  210. },
  211. permissions: withFallback(() => store.notifications?.permissions, defaultSettings.notifications.permissions),
  212. setPermissions(value: boolean) {
  213. setStore("notifications", "permissions", value)
  214. },
  215. errors: withFallback(() => store.notifications?.errors, defaultSettings.notifications.errors),
  216. setErrors(value: boolean) {
  217. setStore("notifications", "errors", value)
  218. },
  219. },
  220. sounds: {
  221. agentEnabled: withFallback(() => store.sounds?.agentEnabled, defaultSettings.sounds.agentEnabled),
  222. setAgentEnabled(value: boolean) {
  223. setStore("sounds", "agentEnabled", value)
  224. },
  225. agent: withFallback(() => store.sounds?.agent, defaultSettings.sounds.agent),
  226. setAgent(value: string) {
  227. setStore("sounds", "agent", value)
  228. },
  229. permissionsEnabled: withFallback(
  230. () => store.sounds?.permissionsEnabled,
  231. defaultSettings.sounds.permissionsEnabled,
  232. ),
  233. setPermissionsEnabled(value: boolean) {
  234. setStore("sounds", "permissionsEnabled", value)
  235. },
  236. permissions: withFallback(() => store.sounds?.permissions, defaultSettings.sounds.permissions),
  237. setPermissions(value: string) {
  238. setStore("sounds", "permissions", value)
  239. },
  240. errorsEnabled: withFallback(() => store.sounds?.errorsEnabled, defaultSettings.sounds.errorsEnabled),
  241. setErrorsEnabled(value: boolean) {
  242. setStore("sounds", "errorsEnabled", value)
  243. },
  244. errors: withFallback(() => store.sounds?.errors, defaultSettings.sounds.errors),
  245. setErrors(value: string) {
  246. setStore("sounds", "errors", value)
  247. },
  248. },
  249. }
  250. },
  251. })