Browse Source

#10 Add settings management with get and update functionality in IdeBridge

paviko 1 month ago
parent
commit
22c6e00486

+ 41 - 0
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/IdeBridge.kt

@@ -376,6 +376,22 @@ object IdeBridge {
                     replyWithPayload(session, id, existing)
                 }
 
+                "settings.get" -> {
+                    replyWithPayload(session, id, readSettings())
+                }
+
+                "settings.update" -> {
+                    val current = readSettings()
+                    val merged = JsonObject().apply {
+                        current.entrySet().forEach { (k, v) -> add(k, v) }
+                        payload?.entrySet()?.forEach { (k, v) -> add(k, v) }
+                    }
+                    val normalized = normalizeSettings(merged)
+                    statePath.mkdirs()
+                    File(statePath, "settings.json").writeText(gson.toJson(normalized))
+                    replyWithPayload(session, id, normalized)
+                }
+
                 else -> replyError(session, id, "Unknown type: $type")
             }
 
@@ -393,6 +409,31 @@ object IdeBridge {
             "opencode"
         )
 
+    private fun readSettings(): JsonObject {
+        val file = File(statePath, "settings.json")
+        val data = try {
+            if (file.exists()) gson.fromJson(file.readText(), JsonObject::class.java) ?: JsonObject()
+            else JsonObject()
+        } catch (_: Throwable) { JsonObject() }
+        return normalizeSettings(data)
+    }
+
+    private fun normalizeSettings(raw: JsonObject): JsonObject {
+        val normalized = JsonObject()
+        raw.entrySet().forEach { (k, v) -> normalized.add(k, v) }
+
+        if (normalized.has("theme")) {
+            val theme = normalized.get("theme")
+            val valid = theme != null && theme.isJsonPrimitive && theme.asJsonPrimitive.isString &&
+                (theme.asString == "light" || theme.asString == "dark")
+            if (!valid) {
+                normalized.remove("theme")
+            }
+        }
+
+        return normalized
+    }
+
     private fun replyWithPayload(session: Session, id: String?, payload: Any) {
         if (id == null) return
         val msg = JsonObject().apply {

+ 47 - 0
hosts/vscode-plugin/src/ui/IdeBridgeServer.ts

@@ -37,6 +37,11 @@ interface Message {
   timestamp: number
 }
 
+type BridgeSettings = {
+  theme?: "light" | "dark"
+  [key: string]: unknown
+}
+
 class IdeBridgeServer {
   private server: http.Server | null = null
   private port: number = 0
@@ -343,6 +348,21 @@ class IdeBridgeServer {
           break
         }
 
+        case "settings.get": {
+          this.replyWithPayload(session, id, this.readSettings())
+          break
+        }
+
+        case "settings.update": {
+          const existing = this.readSettings()
+          const patch = this.normalizeSettings(payload)
+          const merged = this.normalizeSettings({ ...existing, ...patch })
+          fs.mkdirSync(this.statePath(), { recursive: true })
+          fs.writeFileSync(this.settingsPath(), JSON.stringify(merged, null, 2))
+          this.replyWithPayload(session, id, merged)
+          break
+        }
+
         default:
           this.replyError(session, id, `Unknown type: ${type}`)
       }
@@ -359,6 +379,33 @@ class IdeBridgeServer {
     return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"), "opencode")
   }
 
+  private settingsPath(): string {
+    return path.join(this.statePath(), "settings.json")
+  }
+
+  private readSettings(): BridgeSettings {
+    try {
+      const file = this.settingsPath()
+      if (!fs.existsSync(file)) return {}
+      const parsed = JSON.parse(fs.readFileSync(file, "utf-8"))
+      return this.normalizeSettings(parsed)
+    } catch {
+      return {}
+    }
+  }
+
+  private normalizeSettings(input: unknown): BridgeSettings {
+    if (!input || typeof input !== "object" || Array.isArray(input)) {
+      return {}
+    }
+
+    const next = { ...(input as Record<string, unknown>) } as BridgeSettings
+    if (next.theme !== undefined && next.theme !== "light" && next.theme !== "dark") {
+      delete next.theme
+    }
+    return next
+  }
+
   private replyWithPayload(session: Session, id: string | undefined, payload: any): void {
     if (!id) return
     this.broadcastSSE(

+ 40 - 1
packages/opencode/webgui/src/lib/ideBridge.ts

@@ -8,6 +8,11 @@ type Message = {
   error?: string
 }
 
+export type IdeBridgeSettings = {
+  theme?: "light" | "dark"
+  [key: string]: unknown
+}
+
 type Handler = (message: Message) => void
 
 // Parse URL params once at module load
@@ -77,6 +82,12 @@ class IdeBridge {
           window.dispatchEvent(new CustomEvent("opencode:ui-bridge-state", { detail: { state } }))
         } catch {}
       })
+
+      void this.getSettings().then((settings) => {
+        try {
+          window.dispatchEvent(new CustomEvent("opencode:ui-bridge-settings", { detail: { settings } }))
+        } catch {}
+      })
     }
 
     this.eventSource.onmessage = (ev) => {
@@ -171,7 +182,11 @@ class IdeBridge {
   private async doSend(msg: Message, retryCount = 0) {
     if (!bridgeBase || !token) return
 
-    const quiet = msg.type === "uiGetState" || msg.type === "uiSetState"
+    const quiet =
+      msg.type === "uiGetState" ||
+      msg.type === "uiSetState" ||
+      msg.type === "settings.get" ||
+      msg.type === "settings.update"
 
     try {
       const response = await fetch(`${bridgeBase}/send?token=${encodeURIComponent(token)}`, {
@@ -252,6 +267,30 @@ class IdeBridge {
       return false
     }
   }
+
+  async getSettings<T extends IdeBridgeSettings = IdeBridgeSettings>(): Promise<T | null> {
+    try {
+      const res = await this.request<T>("settings.get")
+      const settings = (res as any)?.payload
+      if (!settings || typeof settings !== "object") return null
+      return settings as T
+    } catch {
+      return null
+    }
+  }
+
+  async updateSettings<T extends IdeBridgeSettings = IdeBridgeSettings>(
+    patch: Partial<T>,
+  ): Promise<T | null> {
+    try {
+      const res = await this.request<T>("settings.update", patch)
+      const settings = (res as any)?.payload
+      if (!settings || typeof settings !== "object") return null
+      return settings as T
+    } catch {
+      return null
+    }
+  }
 }
 
 export const ideBridge = new IdeBridge()

+ 39 - 8
packages/opencode/webgui/src/state/ThemeContext.tsx

@@ -1,4 +1,5 @@
 import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
+import { ideBridge, type IdeBridgeSettings } from "../lib/ideBridge"
 
 type Theme = "light" | "dark"
 
@@ -9,26 +10,56 @@ interface ThemeContextValue {
 
 const ThemeContext = createContext<ThemeContextValue | null>(null)
 
+function systemTheme(): Theme {
+  if (typeof window === "undefined") return "light"
+  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
+}
+
+function parseTheme(value: unknown): Theme | null {
+  if (value === "light" || value === "dark") return value
+  return null
+}
+
 export function ThemeProvider({ children }: { children: ReactNode }) {
-  const [theme, setTheme] = useState<Theme>("light")
+  const [theme, setTheme] = useState<Theme>(() => systemTheme())
 
   useEffect(() => {
-    // Apply theme to document root
-    console.log("[ThemeProvider] Applying theme:", theme)
     if (theme === "dark") {
       document.documentElement.classList.add("dark")
     } else {
       document.documentElement.classList.remove("dark")
     }
-    console.log("[ThemeProvider] document.documentElement.classList:", document.documentElement.classList.toString())
   }, [theme])
 
+  useEffect(() => {
+    if (!ideBridge.isInstalled()) return
+
+    const apply = (settings: IdeBridgeSettings | null | undefined) => {
+      const next = parseTheme(settings?.theme)
+      if (!next) return
+      setTheme(next)
+    }
+
+    const handler = (ev: Event) => {
+      const detail = (ev as CustomEvent<{ settings?: IdeBridgeSettings | null }>).detail
+      apply(detail?.settings)
+    }
+
+    window.addEventListener("opencode:ui-bridge-settings", handler)
+    void ideBridge.getSettings().then((settings) => apply(settings))
+
+    return () => {
+      window.removeEventListener("opencode:ui-bridge-settings", handler)
+    }
+  }, [])
+
   const toggleTheme = () => {
-    console.log("[ThemeProvider] Toggle theme called, current:", theme)
     setTheme((prev) => {
-      const newTheme = prev === "light" ? "dark" : "light"
-      console.log("[ThemeProvider] New theme:", newTheme)
-      return newTheme
+      const next = prev === "light" ? "dark" : "light"
+      if (ideBridge.isInstalled()) {
+        void ideBridge.updateSettings({ theme: next })
+      }
+      return next
     })
   }