feat(app): add /reload to reload configuration (#59887)

Co-authored-by: vimtor <36263538+vimtor@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-10-28 16:27:29 +00:00
committed by GitHub
Unverified
parent b6c43a5242
commit 35a4a6876d
3 changed files with 64 additions and 0 deletions
@@ -915,10 +915,32 @@ test("lists slash commands in their built-in order", async ({ page }) => {
"/mcp",
"/model",
"/connect",
"/reload",
"/btw",
])
})
test("/reload reloads the server's configuration once and reports it", async ({ page }) => {
const { editor } = await openSession(page, { name: "ComposerReload", provider: NO_PROVIDER })
const reloaded = Promise.withResolvers<void>()
let reloads = 0
await page.route(/\/api\/location\/reload$/, async (route) => {
reloads++
await reloaded.promise
await route.fulfill({ status: 204 })
})
await editor.fill("/reload")
await expect(page.locator('[data-suggestion-id="config.reload"]')).toHaveAttribute("data-active", "")
await editor.press("Enter")
await expect(page.getByText("Reloading configuration…", { exact: true })).toBeVisible()
reloaded.resolve()
await expect(page.getByText("Configuration reloaded", { exact: true })).toBeVisible()
await expect(page.getByText("Reloading configuration…", { exact: true })).toBeHidden()
await expect(editor).toBeEmpty()
expect(reloads).toBe(1)
})
const followUp = "Add follow-up, / for commands, @ for context…"
for (const row of [
+37
View File
@@ -9,6 +9,10 @@ import { useSessionLayout } from "@/session/session-layout"
import { createSessionOwnership } from "@/session/session-ownership"
import { useWorkspaceLocation } from "@/workspaces/location"
import { useData } from "@/runtime/server/current"
import { useServerSDK } from "@/runtime/server/client"
import { formatServerError } from "@/runtime/server/errors"
import { showToast } from "@/shell/notifications/toast"
import { toaster } from "@opencode/ui/toast"
const withCategory = (category: string) => {
return (option: Omit<CommandOption, "category">): CommandOption => ({
@@ -26,12 +30,14 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
const workspace = useWorkspaceLocation()
const { sessionKey, params } = useSessionLayout()
const data = useData()
const sdk = useServerSDK()
const sessionOwnership = createSessionOwnership(sessionKey)
const model = input.model ?? local.model
const modelCommand = withCategory(language.t("command.category.model"))
const agentCommand = withCategory(language.t("command.category.agent"))
const providerCommand = withCategory(language.t("command.category.provider"))
const viewCommand = withCategory(language.t("command.category.view"))
const serverCommand = withCategory(language.t("command.category.server"))
// Mirrors the TUI's `/connect`, which the Console's setup steps tell people to run.
const connectProvider = async () => {
@@ -40,6 +46,28 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
void dialog.show(() => <DialogConnectProvider directory={workspace().directory} selection={model} />)
}
// Mirrors the TUI's `/reload`. The server rebuilds every loaded location and announces the changed agents,
// commands, models and skills, so the data layer refreshes them without restarting the app.
const reloadConfig = () => {
const pending = showToast({
variant: "loading",
title: language.t("command.config.reload.pending"),
persistent: true,
})
void sdk.api.location
.reload()
.then(() => showToast({ variant: "success", title: language.t("command.config.reload.done") }))
.catch((error) =>
showToast({
variant: "error",
title: language.t("command.config.reload.failed"),
description: formatServerError(error, language.t),
}),
)
.finally(() => toaster.dismiss(pending))
}
const chooseModel = () => {
const Picker = settings.general.newModelPicker() ? DialogSelectModelNext : DialogSelectModel
sessionOwnership.capture().run(() => {
@@ -50,10 +78,12 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
const viewUsage = async () => {
const { DialogUsage } = await import("@/usage/dialog")
const current = model.current()
// Console refuses with 402 only when the workspace can't pay, which the dialog explains.
const last = params.id
? data.session.message.list(params.id)?.findLast((message) => message.type === "assistant")
: undefined
void dialog.show(() => (
<DialogUsage
location={workspace().ref}
@@ -93,6 +123,13 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
slash: "connect",
onSelect: connectProvider,
}),
serverCommand({
id: "config.reload",
title: language.t("command.config.reload"),
description: language.t("command.config.reload.description"),
slash: "reload",
onSelect: reloadConfig,
}),
agentCommand({
id: "agent.cycle",
title: language.t("command.agent.cycle"),
+5
View File
@@ -57,6 +57,11 @@ export const dict = {
"command.provider.connect": "Connect an integration",
"command.provider.connect.description": "Sign in to OpenCode Go, OpenCode Console, or another model provider",
"command.settings.open": "Open settings",
"command.config.reload": "Reload configuration",
"command.config.reload.description": "Reload agents, commands, skills, MCP servers and plugins from your config",
"command.config.reload.pending": "Reloading configuration…",
"command.config.reload.done": "Configuration reloaded",
"command.config.reload.failed": "Couldn't reload configuration",
"command.usage.view": "View usage",
"command.usage.view.description": "See your OpenCode Go allowance and Console budgets",
"command.session.location.cycle": "Cycle session location",