Skip to content

Commit bf59be1

Browse files
committed
feat: add schema-driven settings UI with OmoField component
- Add OmoField component for dynamic form field rendering - Add schema.ts RPC procedures for config schema - Add SchemaField, SchemaRenderer, SchemaSection, DynamicSchemaSection components - Add useOpencodeSchema and useSchemaParser composables - Add SchemaDemoView for schema preview - Refactor all settings sections to use schema-based fields - Add command/environment row UI in McpSection
1 parent 492fd11 commit bf59be1

19 files changed

Lines changed: 2636 additions & 342 deletions

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24.11.1

src/main/rpc/procedures/schema.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { os } from '@orpc/server'
2+
import { app } from 'electron'
3+
import { join } from 'path'
4+
import { readFile, writeFile, mkdir } from 'fs/promises'
5+
import { existsSync } from 'fs'
6+
7+
const OPENCODE_SCHEMA_URL = 'https://opencode.ai/config.json'
8+
const CACHE_DIR = join(app.getPath('userData'), 'cache')
9+
const SCHEMA_CACHE_PATH = join(CACHE_DIR, 'opencode-schema.json')
10+
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
11+
12+
const DEFAULT_SCHEMA: Record<string, unknown> = {
13+
"$schema": "http://json-schema.org/draft-07/schema#",
14+
"type": "object",
15+
"properties": {
16+
"theme": {
17+
"type": "string",
18+
"description": "Theme preset used by the CLI and UI surfaces"
19+
},
20+
"model": {
21+
"type": "string",
22+
"description": "Primary model used for normal coding and reasoning tasks"
23+
},
24+
"small_model": {
25+
"type": "string",
26+
"description": "Lower-cost model for lightweight tasks and fast helper operations"
27+
},
28+
"default_agent": {
29+
"type": "string",
30+
"description": "Agent profile selected by default when no agent is explicitly specified"
31+
},
32+
"username": {
33+
"type": "string",
34+
"description": "Name shown in generated content where a user identity is needed"
35+
},
36+
"logLevel": {
37+
"type": "string",
38+
"enum": ["debug", "info", "warn", "error"],
39+
"description": "Controls log verbosity"
40+
},
41+
"server": {
42+
"type": "object",
43+
"properties": {
44+
"port": { "type": "number" },
45+
"hostname": { "type": "string" },
46+
"cors": { "type": "boolean" }
47+
}
48+
},
49+
"provider": {
50+
"type": "object",
51+
"additionalProperties": {
52+
"type": "object",
53+
"properties": {
54+
"apiKey": { "type": "string" },
55+
"baseURL": { "type": "string" },
56+
"models": { "type": "array", "items": { "type": "string" } }
57+
}
58+
}
59+
},
60+
"mcp": {
61+
"type": "object",
62+
"additionalProperties": {
63+
"type": "object",
64+
"properties": {
65+
"enabled": { "type": "boolean" },
66+
"type": { "type": "string", "enum": ["local", "remote"] },
67+
"command": { "type": "string" },
68+
"url": { "type": "string" },
69+
"timeout": { "type": "number" }
70+
}
71+
}
72+
},
73+
"agent": {
74+
"type": "object",
75+
"additionalProperties": {
76+
"type": "object",
77+
"properties": {
78+
"model": { "type": "string" },
79+
"temperature": { "type": "number" },
80+
"steps": { "type": "number" }
81+
}
82+
}
83+
},
84+
"permission": {
85+
"type": "object",
86+
"properties": {
87+
"read": { "type": "string", "enum": ["ask", "allow", "deny"] },
88+
"edit": { "type": "string", "enum": ["ask", "allow", "deny"] },
89+
"bash": { "type": "string", "enum": ["ask", "allow", "deny"] }
90+
}
91+
},
92+
"compaction": {
93+
"type": "object",
94+
"properties": {
95+
"auto": { "type": "boolean" },
96+
"prune": { "type": "boolean" }
97+
}
98+
},
99+
"experimental": {
100+
"type": "object",
101+
"properties": {
102+
"disable_paste_summary": { "type": "boolean" },
103+
"batch_tool": { "type": "boolean" },
104+
"continue_loop_on_deny": { "type": "boolean" }
105+
}
106+
},
107+
"plugin": {
108+
"type": "array",
109+
"items": { "type": "string" }
110+
}
111+
}
112+
}
113+
114+
interface SchemaCache {
115+
schema: Record<string, unknown>
116+
fetchedAt: string
117+
}
118+
119+
async function ensureCacheDir(): Promise<void> {
120+
if (!existsSync(CACHE_DIR)) {
121+
await mkdir(CACHE_DIR, { recursive: true })
122+
}
123+
}
124+
125+
async function fetchSchemaFromRemote(): Promise<Record<string, unknown>> {
126+
const response = await fetch(OPENCODE_SCHEMA_URL)
127+
if (!response.ok) {
128+
throw new Error(`Failed to fetch schema: ${response.status} ${response.statusText}`)
129+
}
130+
return response.json() as Promise<Record<string, unknown>>
131+
}
132+
133+
async function readCachedSchema(): Promise<SchemaCache | null> {
134+
try {
135+
if (!existsSync(SCHEMA_CACHE_PATH)) {
136+
return null
137+
}
138+
const content = await readFile(SCHEMA_CACHE_PATH, 'utf-8')
139+
return JSON.parse(content) as SchemaCache
140+
} catch {
141+
return null
142+
}
143+
}
144+
145+
async function writeCachedSchema(schema: Record<string, unknown>): Promise<void> {
146+
await ensureCacheDir()
147+
const cache: SchemaCache = {
148+
schema,
149+
fetchedAt: new Date().toISOString(),
150+
}
151+
await writeFile(SCHEMA_CACHE_PATH, JSON.stringify(cache, null, 2), 'utf-8')
152+
}
153+
154+
function isCacheExpired(cache: SchemaCache): boolean {
155+
const fetchedAt = new Date(cache.fetchedAt).getTime()
156+
const now = Date.now()
157+
return now - fetchedAt > CACHE_MAX_AGE_MS
158+
}
159+
160+
export const getOpencodeSchema = os.handler(async () => {
161+
console.log('getOpencodeSchema called')
162+
try {
163+
const cached = await readCachedSchema()
164+
console.log('Cache check:', cached ? `cached at ${cached.fetchedAt}` : 'no cache')
165+
166+
if (cached && !isCacheExpired(cached)) {
167+
console.log('Returning cached schema')
168+
return { schema: cached.schema, fromCache: true }
169+
}
170+
171+
console.log('Fetching schema from remote...')
172+
const schema = await fetchSchemaFromRemote()
173+
await writeCachedSchema(schema)
174+
console.log('Schema fetched and cached successfully')
175+
return { schema, fromCache: false }
176+
} catch (error) {
177+
console.error('Schema fetch failed:', error)
178+
const cached = await readCachedSchema()
179+
if (cached) {
180+
console.log('Returning cached schema after fetch error')
181+
return { schema: cached.schema, fromCache: true, error: String(error) }
182+
}
183+
console.log('Returning default schema after fetch error')
184+
return { schema: DEFAULT_SCHEMA, fromCache: false, error: String(error) }
185+
}
186+
})
187+
188+
export const refreshOpencodeSchema = os.handler(async () => {
189+
const schema = await fetchSchemaFromRemote()
190+
await writeCachedSchema(schema)
191+
return { schema, fromCache: false }
192+
})

0 commit comments

Comments
 (0)