Skip to content

Commit 2f35c40

Browse files
committed
chore(app): global config changes
1 parent 6650aa6 commit 2f35c40

5 files changed

Lines changed: 1637 additions & 375 deletions

File tree

packages/app/src/context/global-sync.tsx

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,74 @@ function createGlobalSync() {
188188
config: {},
189189
reload: undefined,
190190
})
191-
let bootstrapQueue: string[] = []
191+
192+
const queued = new Set<string>()
193+
let root = false
194+
let running = false
195+
let timer: ReturnType<typeof setTimeout> | undefined
196+
197+
const paused = () => untrack(() => globalStore.reload) !== undefined
198+
199+
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
200+
201+
const take = (count: number) => {
202+
if (queued.size === 0) return [] as string[]
203+
const items: string[] = []
204+
for (const item of queued) {
205+
queued.delete(item)
206+
items.push(item)
207+
if (items.length >= count) break
208+
}
209+
return items
210+
}
211+
212+
const schedule = () => {
213+
if (timer) return
214+
timer = setTimeout(() => {
215+
timer = undefined
216+
void drain()
217+
}, 0)
218+
}
219+
220+
const push = (directory: string) => {
221+
if (!directory) return
222+
queued.add(directory)
223+
if (paused()) return
224+
schedule()
225+
}
226+
227+
const refresh = () => {
228+
root = true
229+
if (paused()) return
230+
schedule()
231+
}
232+
233+
async function drain() {
234+
if (running) return
235+
running = true
236+
try {
237+
while (true) {
238+
if (paused()) return
239+
240+
if (root) {
241+
root = false
242+
await bootstrap()
243+
await tick()
244+
continue
245+
}
246+
247+
const dirs = take(2)
248+
if (dirs.length === 0) return
249+
250+
await Promise.all(dirs.map((dir) => bootstrapInstance(dir)))
251+
await tick()
252+
}
253+
} finally {
254+
running = false
255+
if (paused()) return
256+
if (root || queued.size) schedule()
257+
}
258+
}
192259

193260
createEffect(() => {
194261
if (!projectCacheReady()) return
@@ -210,14 +277,8 @@ function createGlobalSync() {
210277

211278
createEffect(() => {
212279
if (globalStore.reload !== "complete") return
213-
if (bootstrapQueue.length) {
214-
for (const directory of bootstrapQueue) {
215-
bootstrapInstance(directory)
216-
}
217-
bootstrap()
218-
}
219-
bootstrapQueue = []
220280
setGlobalStore("reload", undefined)
281+
refresh()
221282
})
222283

223284
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
@@ -584,9 +645,8 @@ function createGlobalSync() {
584645
if (directory === "global") {
585646
switch (event?.type) {
586647
case "global.disposed": {
587-
if (globalStore.reload) return
588-
bootstrap()
589-
break
648+
refresh()
649+
return
590650
}
591651
case "project.updated": {
592652
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
@@ -647,12 +707,8 @@ function createGlobalSync() {
647707

648708
switch (event.type) {
649709
case "server.instance.disposed": {
650-
if (globalStore.reload) {
651-
bootstrapQueue.push(directory)
652-
return
653-
}
654-
bootstrapInstance(directory)
655-
break
710+
push(directory)
711+
return
656712
}
657713
case "session.created": {
658714
const info = event.properties.info
@@ -893,6 +949,10 @@ function createGlobalSync() {
893949
}
894950
})
895951
onCleanup(unsub)
952+
onCleanup(() => {
953+
if (!timer) return
954+
clearTimeout(timer)
955+
})
896956

897957
async function bootstrap() {
898958
const health = await globalSDK.client.global
@@ -916,7 +976,7 @@ function createGlobalSync() {
916976
}),
917977
),
918978
retry(() =>
919-
globalSDK.client.config.get().then((x) => {
979+
globalSDK.client.global.config.get().then((x) => {
920980
setGlobalStore("config", x.data!)
921981
}),
922982
),
@@ -999,13 +1059,13 @@ function createGlobalSync() {
9991059
},
10001060
child,
10011061
bootstrap,
1002-
updateConfig: async (config: Config) => {
1062+
updateConfig: (config: Config) => {
10031063
setGlobalStore("reload", "pending")
1004-
const response = await globalSDK.client.config.update({ config })
1005-
setTimeout(() => {
1006-
setGlobalStore("reload", "complete")
1007-
}, 1000)
1008-
return response
1064+
return globalSDK.client.global.config.update({ config }).finally(() => {
1065+
setTimeout(() => {
1066+
setGlobalStore("reload", "complete")
1067+
}, 1000)
1068+
})
10091069
},
10101070
project: {
10111071
loadSessions,

packages/opencode/src/server/routes/global.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Hono } from "hono"
2-
import { describeRoute, resolver } from "hono-openapi"
2+
import { describeRoute, resolver, validator } from "hono-openapi"
33
import { streamSSE } from "hono/streaming"
44
import z from "zod"
55
import { BusEvent } from "@/bus/bus-event"
@@ -8,6 +8,8 @@ import { Instance } from "../../project/instance"
88
import { Installation } from "@/installation"
99
import { Log } from "../../util/log"
1010
import { lazy } from "../../util/lazy"
11+
import { Config } from "../../config/config"
12+
import { errors } from "../error"
1113

1214
const log = Log.create({ service: "server" })
1315

@@ -103,6 +105,52 @@ export const GlobalRoutes = lazy(() =>
103105
})
104106
},
105107
)
108+
.get(
109+
"/config",
110+
describeRoute({
111+
summary: "Get global configuration",
112+
description: "Retrieve the current global OpenCode configuration settings and preferences.",
113+
operationId: "global.config.get",
114+
responses: {
115+
200: {
116+
description: "Get global config info",
117+
content: {
118+
"application/json": {
119+
schema: resolver(Config.Info),
120+
},
121+
},
122+
},
123+
},
124+
}),
125+
async (c) => {
126+
return c.json(await Config.getGlobal())
127+
},
128+
)
129+
.patch(
130+
"/config",
131+
describeRoute({
132+
summary: "Update global configuration",
133+
description: "Update global OpenCode configuration settings and preferences.",
134+
operationId: "global.config.update",
135+
responses: {
136+
200: {
137+
description: "Successfully updated global config",
138+
content: {
139+
"application/json": {
140+
schema: resolver(Config.Info),
141+
},
142+
},
143+
},
144+
...errors(400),
145+
},
146+
}),
147+
validator("json", Config.Info),
148+
async (c) => {
149+
const config = c.req.valid("json")
150+
await Config.updateGlobal(config)
151+
return c.json(await Config.getGlobal())
152+
},
153+
)
106154
.post(
107155
"/dispose",
108156
describeRoute({

packages/sdk/js/src/v2/gen/sdk.gen.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
AuthSetErrors,
1515
AuthSetResponses,
1616
CommandListResponses,
17-
Config as Config2,
17+
Config as Config3,
1818
ConfigGetResponses,
1919
ConfigProvidersResponses,
2020
ConfigUpdateErrors,
@@ -34,6 +34,9 @@ import type {
3434
FindSymbolsResponses,
3535
FindTextResponses,
3636
FormatterStatusResponses,
37+
GlobalConfigGetResponses,
38+
GlobalConfigUpdateErrors,
39+
GlobalConfigUpdateResponses,
3740
GlobalDisposeResponses,
3841
GlobalEventResponses,
3942
GlobalHealthResponses,
@@ -215,6 +218,44 @@ class HeyApiRegistry<T> {
215218
}
216219
}
217220

221+
export class Config extends HeyApiClient {
222+
/**
223+
* Get global configuration
224+
*
225+
* Retrieve the current global OpenCode configuration settings and preferences.
226+
*/
227+
public get<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
228+
return (options?.client ?? this.client).get<GlobalConfigGetResponses, unknown, ThrowOnError>({
229+
url: "/global/config",
230+
...options,
231+
})
232+
}
233+
234+
/**
235+
* Update global configuration
236+
*
237+
* Update global OpenCode configuration settings and preferences.
238+
*/
239+
public update<ThrowOnError extends boolean = false>(
240+
parameters?: {
241+
config?: Config3
242+
},
243+
options?: Options<never, ThrowOnError>,
244+
) {
245+
const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }])
246+
return (options?.client ?? this.client).patch<GlobalConfigUpdateResponses, GlobalConfigUpdateErrors, ThrowOnError>({
247+
url: "/global/config",
248+
...options,
249+
...params,
250+
headers: {
251+
"Content-Type": "application/json",
252+
...options?.headers,
253+
...params.headers,
254+
},
255+
})
256+
}
257+
}
258+
218259
export class Global extends HeyApiClient {
219260
/**
220261
* Get health
@@ -251,6 +292,11 @@ export class Global extends HeyApiClient {
251292
...options,
252293
})
253294
}
295+
296+
private _config?: Config
297+
get config(): Config {
298+
return (this._config ??= new Config({ client: this.client }))
299+
}
254300
}
255301

256302
export class Project extends HeyApiClient {
@@ -541,7 +587,7 @@ export class Pty extends HeyApiClient {
541587
}
542588
}
543589

544-
export class Config extends HeyApiClient {
590+
export class Config2 extends HeyApiClient {
545591
/**
546592
* Get configuration
547593
*
@@ -569,7 +615,7 @@ export class Config extends HeyApiClient {
569615
public update<ThrowOnError extends boolean = false>(
570616
parameters?: {
571617
directory?: string
572-
config?: Config2
618+
config?: Config3
573619
},
574620
options?: Options<never, ThrowOnError>,
575621
) {
@@ -3168,9 +3214,9 @@ export class OpencodeClient extends HeyApiClient {
31683214
return (this._pty ??= new Pty({ client: this.client }))
31693215
}
31703216

3171-
private _config?: Config
3172-
get config(): Config {
3173-
return (this._config ??= new Config({ client: this.client }))
3217+
private _config?: Config2
3218+
get config(): Config2 {
3219+
return (this._config ??= new Config2({ client: this.client }))
31743220
}
31753221

31763222
private _tool?: Tool

0 commit comments

Comments
 (0)