From 2fe7d13e692bf9053466a3bd0a23ff3d6de0bc03 Mon Sep 17 00:00:00 2001 From: Yuku Kotani Date: Sun, 2 Nov 2025 00:14:39 +0900 Subject: [PATCH] Add formatter status display to TUI status dialog (#3701) --- .../cli/cmd/tui/component/dialog-status.tsx | 26 +++++++++++++- .../opencode/src/cli/cmd/tui/context/sync.tsx | 4 +++ packages/opencode/src/format/index.ts | 26 ++++++++++++++ packages/opencode/src/server/server.ts | 21 +++++++++++ packages/sdk/js/src/gen/sdk.gen.ts | 17 +++++++++ packages/sdk/js/src/gen/types.gen.ts | 35 +++++++++++++++++++ 6 files changed, 128 insertions(+), 1 deletion(-) 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 c2faddaf..e958580e 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 765fb619..b0c2ea86 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 19790c43..b4294093 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 deb3ed49..2a070ec2 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 1dcdd806..b76b6996 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 63673e76..fb0371cd 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