chore(app): global config changes

This commit is contained in:
adamelmore
2026-01-27 08:53:48 -06:00
parent 6650aa6f35
commit 2f35c40bb5
5 changed files with 1637 additions and 375 deletions

View File

@@ -188,7 +188,74 @@ function createGlobalSync() {
config: {}, config: {},
reload: undefined, reload: undefined,
}) })
let bootstrapQueue: string[] = []
const queued = new Set<string>()
let root = false
let running = false
let timer: ReturnType<typeof setTimeout> | undefined
const paused = () => untrack(() => globalStore.reload) !== undefined
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
const take = (count: number) => {
if (queued.size === 0) return [] as string[]
const items: string[] = []
for (const item of queued) {
queued.delete(item)
items.push(item)
if (items.length >= count) break
}
return items
}
const schedule = () => {
if (timer) return
timer = setTimeout(() => {
timer = undefined
void drain()
}, 0)
}
const push = (directory: string) => {
if (!directory) return
queued.add(directory)
if (paused()) return
schedule()
}
const refresh = () => {
root = true
if (paused()) return
schedule()
}
async function drain() {
if (running) return
running = true
try {
while (true) {
if (paused()) return
if (root) {
root = false
await bootstrap()
await tick()
continue
}
const dirs = take(2)
if (dirs.length === 0) return
await Promise.all(dirs.map((dir) => bootstrapInstance(dir)))
await tick()
}
} finally {
running = false
if (paused()) return
if (root || queued.size) schedule()
}
}
createEffect(() => { createEffect(() => {
if (!projectCacheReady()) return if (!projectCacheReady()) return
@@ -210,14 +277,8 @@ function createGlobalSync() {
createEffect(() => { createEffect(() => {
if (globalStore.reload !== "complete") return if (globalStore.reload !== "complete") return
if (bootstrapQueue.length) {
for (const directory of bootstrapQueue) {
bootstrapInstance(directory)
}
bootstrap()
}
bootstrapQueue = []
setGlobalStore("reload", undefined) setGlobalStore("reload", undefined)
refresh()
}) })
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {} const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
@@ -584,9 +645,8 @@ function createGlobalSync() {
if (directory === "global") { if (directory === "global") {
switch (event?.type) { switch (event?.type) {
case "global.disposed": { case "global.disposed": {
if (globalStore.reload) return refresh()
bootstrap() return
break
} }
case "project.updated": { case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
@@ -647,12 +707,8 @@ function createGlobalSync() {
switch (event.type) { switch (event.type) {
case "server.instance.disposed": { case "server.instance.disposed": {
if (globalStore.reload) { push(directory)
bootstrapQueue.push(directory) return
return
}
bootstrapInstance(directory)
break
} }
case "session.created": { case "session.created": {
const info = event.properties.info const info = event.properties.info
@@ -893,6 +949,10 @@ function createGlobalSync() {
} }
}) })
onCleanup(unsub) onCleanup(unsub)
onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})
async function bootstrap() { async function bootstrap() {
const health = await globalSDK.client.global const health = await globalSDK.client.global
@@ -916,7 +976,7 @@ function createGlobalSync() {
}), }),
), ),
retry(() => retry(() =>
globalSDK.client.config.get().then((x) => { globalSDK.client.global.config.get().then((x) => {
setGlobalStore("config", x.data!) setGlobalStore("config", x.data!)
}), }),
), ),
@@ -999,13 +1059,13 @@ function createGlobalSync() {
}, },
child, child,
bootstrap, bootstrap,
updateConfig: async (config: Config) => { updateConfig: (config: Config) => {
setGlobalStore("reload", "pending") setGlobalStore("reload", "pending")
const response = await globalSDK.client.config.update({ config }) return globalSDK.client.global.config.update({ config }).finally(() => {
setTimeout(() => { setTimeout(() => {
setGlobalStore("reload", "complete") setGlobalStore("reload", "complete")
}, 1000) }, 1000)
return response })
}, },
project: { project: {
loadSessions, loadSessions,

View File

@@ -1,5 +1,5 @@
import { Hono } from "hono" import { Hono } from "hono"
import { describeRoute, resolver } from "hono-openapi" import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming" import { streamSSE } from "hono/streaming"
import z from "zod" import z from "zod"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
@@ -8,6 +8,8 @@ import { Instance } from "../../project/instance"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { Log } from "../../util/log" import { Log } from "../../util/log"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
import { Config } from "../../config/config"
import { errors } from "../error"
const log = Log.create({ service: "server" }) const log = Log.create({ service: "server" })
@@ -103,6 +105,52 @@ export const GlobalRoutes = lazy(() =>
}) })
}, },
) )
.get(
"/config",
describeRoute({
summary: "Get global configuration",
description: "Retrieve the current global OpenCode configuration settings and preferences.",
operationId: "global.config.get",
responses: {
200: {
description: "Get global config info",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
},
}),
async (c) => {
return c.json(await Config.getGlobal())
},
)
.patch(
"/config",
describeRoute({
summary: "Update global configuration",
description: "Update global OpenCode configuration settings and preferences.",
operationId: "global.config.update",
responses: {
200: {
description: "Successfully updated global config",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
...errors(400),
},
}),
validator("json", Config.Info),
async (c) => {
const config = c.req.valid("json")
await Config.updateGlobal(config)
return c.json(await Config.getGlobal())
},
)
.post( .post(
"/dispose", "/dispose",
describeRoute({ describeRoute({

View File

@@ -14,7 +14,7 @@ import type {
AuthSetErrors, AuthSetErrors,
AuthSetResponses, AuthSetResponses,
CommandListResponses, CommandListResponses,
Config as Config2, Config as Config3,
ConfigGetResponses, ConfigGetResponses,
ConfigProvidersResponses, ConfigProvidersResponses,
ConfigUpdateErrors, ConfigUpdateErrors,
@@ -34,6 +34,9 @@ import type {
FindSymbolsResponses, FindSymbolsResponses,
FindTextResponses, FindTextResponses,
FormatterStatusResponses, FormatterStatusResponses,
GlobalConfigGetResponses,
GlobalConfigUpdateErrors,
GlobalConfigUpdateResponses,
GlobalDisposeResponses, GlobalDisposeResponses,
GlobalEventResponses, GlobalEventResponses,
GlobalHealthResponses, GlobalHealthResponses,
@@ -215,6 +218,44 @@ class HeyApiRegistry<T> {
} }
} }
export class Config extends HeyApiClient {
/**
* Get global configuration
*
* Retrieve the current global OpenCode configuration settings and preferences.
*/
public get<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
return (options?.client ?? this.client).get<GlobalConfigGetResponses, unknown, ThrowOnError>({
url: "/global/config",
...options,
})
}
/**
* Update global configuration
*
* Update global OpenCode configuration settings and preferences.
*/
public update<ThrowOnError extends boolean = false>(
parameters?: {
config?: Config3
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }])
return (options?.client ?? this.client).patch<GlobalConfigUpdateResponses, GlobalConfigUpdateErrors, ThrowOnError>({
url: "/global/config",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Global extends HeyApiClient { export class Global extends HeyApiClient {
/** /**
* Get health * Get health
@@ -251,6 +292,11 @@ export class Global extends HeyApiClient {
...options, ...options,
}) })
} }
private _config?: Config
get config(): Config {
return (this._config ??= new Config({ client: this.client }))
}
} }
export class Project extends HeyApiClient { export class Project extends HeyApiClient {
@@ -541,7 +587,7 @@ export class Pty extends HeyApiClient {
} }
} }
export class Config extends HeyApiClient { export class Config2 extends HeyApiClient {
/** /**
* Get configuration * Get configuration
* *
@@ -569,7 +615,7 @@ export class Config extends HeyApiClient {
public update<ThrowOnError extends boolean = false>( public update<ThrowOnError extends boolean = false>(
parameters?: { parameters?: {
directory?: string directory?: string
config?: Config2 config?: Config3
}, },
options?: Options<never, ThrowOnError>, options?: Options<never, ThrowOnError>,
) { ) {
@@ -3168,9 +3214,9 @@ export class OpencodeClient extends HeyApiClient {
return (this._pty ??= new Pty({ client: this.client })) return (this._pty ??= new Pty({ client: this.client }))
} }
private _config?: Config private _config?: Config2
get config(): Config { get config(): Config2 {
return (this._config ??= new Config({ client: this.client })) return (this._config ??= new Config2({ client: this.client }))
} }
private _tool?: Tool private _tool?: Tool

View File

@@ -930,21 +930,6 @@ export type GlobalEvent = {
payload: Event payload: Event
} }
export type BadRequestError = {
data: unknown
errors: Array<{
[key: string]: unknown
}>
success: false
}
export type NotFoundError = {
name: "NotFoundError"
data: {
message: string
}
}
/** /**
* Custom keybind configurations * Custom keybind configurations
*/ */
@@ -1826,6 +1811,21 @@ export type Config = {
} }
} }
export type BadRequestError = {
data: unknown
errors: Array<{
[key: string]: unknown
}>
success: false
}
export type NotFoundError = {
name: "NotFoundError"
data: {
message: string
}
}
export type Model = { export type Model = {
id: string id: string
providerID: string providerID: string
@@ -2199,6 +2199,47 @@ export type GlobalEventResponses = {
export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses]
export type GlobalConfigGetData = {
body?: never
path?: never
query?: never
url: "/global/config"
}
export type GlobalConfigGetResponses = {
/**
* Get global config info
*/
200: Config
}
export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses]
export type GlobalConfigUpdateData = {
body?: Config
path?: never
query?: never
url: "/global/config"
}
export type GlobalConfigUpdateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors]
export type GlobalConfigUpdateResponses = {
/**
* Successfully updated global config
*/
200: Config
}
export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses]
export type GlobalDisposeData = { export type GlobalDisposeData = {
body?: never body?: never
path?: never path?: never

File diff suppressed because it is too large Load Diff