diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
index c2faddafa6..e958580e0c 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
@@ -1,7 +1,7 @@
import { TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useSync } from "@tui/context/sync"
-import { For, Match, Switch, Show } from "solid-js"
+import { For, Match, Switch, Show, createMemo } from "solid-js"
export type DialogStatusProps = {}
@@ -9,6 +9,8 @@ export function DialogStatus() {
const sync = useSync()
const { theme } = useTheme()
+ const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
+
return (
@@ -73,6 +75,28 @@ export function DialogStatus() {
)}
+ 0} fallback={No Formatters}>
+
+ {enabledFormatters().length} Formatters
+
+ {(item) => (
+
+
+ •
+
+
+ {item.name}
+
+
+ )}
+
+
+
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index 765fb61961..b0c2ea869d 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -10,6 +10,7 @@ import type {
Permission,
LspStatus,
McpStatus,
+ FormatterStatus,
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
@@ -42,6 +43,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
mcp: {
[key: string]: McpStatus
}
+ formatter: FormatterStatus[]
}>({
config: {},
ready: false,
@@ -55,6 +57,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
part: {},
lsp: [],
mcp: {},
+ formatter: [],
})
const sdk = useSDK()
@@ -220,6 +223,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
+ sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
])
const result = {
diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts
index 19790c438e..b429409349 100644
--- a/packages/opencode/src/format/index.ts
+++ b/packages/opencode/src/format/index.ts
@@ -2,6 +2,7 @@ import { Bus } from "../bus"
import { File } from "../file"
import { Log } from "../util/log"
import path from "path"
+import z from "zod"
import * as Formatter from "./formatter"
import { Config } from "../config/config"
@@ -11,6 +12,17 @@ import { Instance } from "../project/instance"
export namespace Format {
const log = Log.create({ service: "format" })
+ export const Status = z
+ .object({
+ name: z.string(),
+ extensions: z.string().array(),
+ enabled: z.boolean(),
+ })
+ .meta({
+ ref: "FormatterStatus",
+ })
+ export type Status = z.infer
+
const state = Instance.state(async () => {
const enabled: Record = {}
const cfg = await Config.get()
@@ -62,6 +74,20 @@ export namespace Format {
return result
}
+ export async function status() {
+ const s = await state()
+ const result: Status[] = []
+ for (const formatter of Object.values(s.formatters)) {
+ const enabled = await isEnabled(formatter)
+ result.push({
+ name: formatter.name,
+ extensions: formatter.extensions,
+ enabled,
+ })
+ }
+ return result
+ }
+
export function init() {
log.info("init")
Bus.subscribe(File.Event.Edited, async (payload) => {
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index deb3ed4932..2a070ec232 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -20,6 +20,7 @@ import { Ripgrep } from "../file/ripgrep"
import { Config } from "../config/config"
import { File } from "../file"
import { LSP } from "../lsp"
+import { Format } from "../format"
import { MessageV2 } from "../session/message-v2"
import { TuiRoute } from "./tui"
import { Permission } from "../permission"
@@ -1336,6 +1337,26 @@ export namespace Server {
return c.json(await LSP.status())
},
)
+ .get(
+ "/formatter",
+ describeRoute({
+ description: "Get formatter status",
+ operationId: "formatter.status",
+ responses: {
+ 200: {
+ description: "Formatter status",
+ content: {
+ "application/json": {
+ schema: resolver(Format.Status.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await Format.status())
+ },
+ )
.post(
"/tui/append-prompt",
describeRoute({
diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts
index 1dcdd80670..b76b6996f6 100644
--- a/packages/sdk/js/src/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/gen/sdk.gen.ts
@@ -107,6 +107,8 @@ import type {
McpStatusResponses,
LspStatusData,
LspStatusResponses,
+ FormatterStatusData,
+ FormatterStatusResponses,
TuiAppendPromptData,
TuiAppendPromptResponses,
TuiAppendPromptErrors,
@@ -773,6 +775,20 @@ class Lsp extends _HeyApiClient {
}
}
+class Formatter extends _HeyApiClient {
+ /**
+ * Get formatter status
+ */
+ public status(
+ options?: Options,
+ ) {
+ return (options?.client ?? this._client).get({
+ url: "/formatter",
+ ...options,
+ })
+ }
+}
+
class Control extends _HeyApiClient {
/**
* Get the next TUI request from the queue
@@ -1023,6 +1039,7 @@ export class OpencodeClient extends _HeyApiClient {
app = new App({ client: this._client })
mcp = new Mcp({ client: this._client })
lsp = new Lsp({ client: this._client })
+ formatter = new Formatter({ client: this._client })
tui = new Tui({ client: this._client })
auth = new Auth({ client: this._client })
event = new Event({ client: this._client })
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 63673e7626..fb0371cda6 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -1070,6 +1070,12 @@ export type LspStatus = {
status: "connected" | "error"
}
+export type FormatterStatus = {
+ name: string
+ extensions: Array
+ enabled: boolean
+}
+
export type EventTuiPromptAppend = {
type: "tui.prompt.append"
properties: {
@@ -1248,6 +1254,16 @@ export type EventTodoUpdated = {
}
}
+export type EventCommandExecuted = {
+ type: "command.executed"
+ properties: {
+ name: string
+ sessionID: string
+ arguments: string
+ messageID: string
+ }
+}
+
export type EventSessionIdle = {
type: "session.idle"
properties: {
@@ -1310,6 +1326,7 @@ export type Event =
| EventFileEdited
| EventFileWatcherUpdated
| EventTodoUpdated
+ | EventCommandExecuted
| EventSessionIdle
| EventSessionCreated
| EventSessionUpdated
@@ -2511,6 +2528,24 @@ export type LspStatusResponses = {
export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses]
+export type FormatterStatusData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/formatter"
+}
+
+export type FormatterStatusResponses = {
+ /**
+ * Formatter status
+ */
+ 200: Array
+}
+
+export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
+
export type TuiAppendPromptData = {
body?: {
text: string