Browse Source

Add formatter status display to TUI status dialog (#3701)

Yuku Kotani 3 months ago
parent
commit
2fe7d13e69

+ 25 - 1
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 (
     <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
       <box flexDirection="row" justifyContent="space-between">
@@ -73,6 +75,28 @@ export function DialogStatus() {
           </For>
         </box>
       )}
+      <Show when={enabledFormatters().length > 0} fallback={<text>No Formatters</text>}>
+        <box>
+          <text>{enabledFormatters().length} Formatters</text>
+          <For each={enabledFormatters()}>
+            {(item) => (
+              <box flexDirection="row" gap={1}>
+                <text
+                  flexShrink={0}
+                  style={{
+                    fg: theme.success,
+                  }}
+                >
+                  •
+                </text>
+                <text wrapMode="word">
+                  <b>{item.name}</b>
+                </text>
+              </box>
+            )}
+          </For>
+        </box>
+      </Show>
     </box>
   )
 }

+ 4 - 0
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 = {

+ 26 - 0
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<typeof Status>
+
   const state = Instance.state(async () => {
     const enabled: Record<string, boolean> = {}
     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) => {

+ 21 - 0
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({

+ 17 - 0
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<ThrowOnError extends boolean = false>(
+    options?: Options<FormatterStatusData, ThrowOnError>,
+  ) {
+    return (options?.client ?? this._client).get<FormatterStatusResponses, unknown, ThrowOnError>({
+      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 })

+ 35 - 0
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<string>
+  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<FormatterStatus>
+}
+
+export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
+
 export type TuiAppendPromptData = {
   body?: {
     text: string