From 28a946bcb9e907de88507fed40352faa03b761aa Mon Sep 17 00:00:00 2001 From: Z User Date: Mon, 15 Jun 2026 22:04:22 +0000 Subject: [PATCH] fix: add structured logging to frecency, stash, KV, clipboard, and remaining TUI console calls Replace all remaining bare console.log/error/warn in TUI code with structured Log.create() loggers. This covers: - frecency.tsx: log file IO failures on init, append, and trim writes - stash.tsx: log file IO failures on init rewrite, push, pop, remove - kv.tsx: log read/write failures for persistent KV state - clipboard.ts: replace debug console.log method selection with log.debug - app.tsx: log clipboard copy failures and plugin load failures - prompt/index.tsx: log session creation failures - dialog-mcp.tsx: log MCP toggle and status refresh failures - plugin/slots.tsx: log plugin slot errors The plugin/runtime.ts dual logging (log + console) is intentionally preserved as it aids plugin developer debugging. --- packages/opencode/src/cli/cmd/tui/app.tsx | 10 ++++---- .../src/cli/cmd/tui/component/dialog-mcp.tsx | 8 ++++--- .../cli/cmd/tui/component/prompt/frecency.tsx | 16 +++++++++---- .../cli/cmd/tui/component/prompt/index.tsx | 6 +++-- .../cli/cmd/tui/component/prompt/stash.tsx | 24 ++++++++++++++----- .../opencode/src/cli/cmd/tui/context/kv.tsx | 10 ++++---- .../opencode/src/cli/cmd/tui/plugin/slots.tsx | 7 ++++-- .../src/cli/cmd/tui/util/clipboard.ts | 15 +++++++----- 8 files changed, 65 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 31897111..7eea5458 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -61,17 +61,19 @@ import { DialogSelect } from "./ui/dialog-select" import { Provider } from "@/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" -import { Process } from "@/util" +import { Process, Log } from "@/util" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" import { FormatError, FormatUnknownError } from "@/cli/error" import { isPlainTerminal } from "./util/terminal" - import type { EventSource } from "./context/sdk" + import { DialogVariant } from "./component/dialog-variant" +const log = Log.create({ service: "tui-app" }) + function rendererConfig(_config: TuiConfig.Info, plainTerminal: boolean): CliRendererConfig { const mouseEnabled = !plainTerminal && !Flag.MIMOCODE_DISABLE_MOUSE && (_config.mouse ?? true) @@ -99,7 +101,7 @@ function rendererConfig(_config: TuiConfig.Info, plainTerminal: boolean): CliRen keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], onCopySelection: (text) => { Clipboard.copy(text).catch((error) => { - console.error(`Failed to copy console selection to clipboard: ${error}`) + log.error("Failed to copy console selection to clipboard", { error }) }) }, }, @@ -272,7 +274,7 @@ function App(props: { onSnapshot?: () => Promise }) { config: tuiConfig, }) .catch((error) => { - console.error("Failed to load TUI plugins", error) + log.error("Failed to load TUI plugins", { error }) }) .finally(() => { setReady(true) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx index e3e80c0f..ca70dd99 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx @@ -4,10 +4,12 @@ import { useSync } from "@tui/context/sync" import { map, pipe, entries, sortBy } from "remeda" import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select" import { useTheme } from "../context/theme" -import { Keybind } from "@/util" +import { Keybind, Log } from "@/util" import { TextAttributes } from "@opentui/core" import { useSDK } from "@tui/context/sdk" +const log = Log.create({ service: "dialog-mcp" }) + function Status(props: { enabled: boolean; loading: boolean }) { const { theme } = useTheme() if (props.loading) { @@ -61,10 +63,10 @@ export function DialogMcp() { if (status.data) { sync.set("mcp", status.data) } else { - console.error("Failed to refresh MCP status: no data returned") + log.error("Failed to refresh MCP status: no data returned") } } catch (error) { - console.error("Failed to toggle MCP:", error) + log.error("Failed to toggle MCP", { error }) } finally { setLoading(null) } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx index 929f3a07..ebfec1c0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx @@ -1,11 +1,13 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util" +import { Filesystem, Log } from "@/util" import { onMount } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "../../context/helper" import { appendFile, writeFile } from "fs/promises" +const log = Log.create({ service: "frecency" }) + function calculateFrecency(entry?: { frequency: number; lastOpen: number }): number { if (!entry) return 0 const daysSince = (Date.now() - entry.lastOpen) / 86400000 // ms per day @@ -54,7 +56,9 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont if (sorted.length > 0) { const content = sorted.map((entry) => JSON.stringify(entry)).join("\n") + "\n" - writeFile(frecencyPath, content).catch(() => {}) + writeFile(frecencyPath, content).catch((err) => + log.error("failed to write frecency file on init", { path: frecencyPath, err }), + ) } }) @@ -69,7 +73,9 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont lastOpen: Date.now(), } setStore("data", absolutePath, newEntry) - appendFile(frecencyPath, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {}) + appendFile(frecencyPath, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch((err) => + log.error("failed to append frecency entry", { path: frecencyPath, err }), + ) if (Object.keys(store.data).length > MAX_FRECENCY_ENTRIES) { const sorted = Object.entries(store.data) @@ -77,7 +83,9 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont .slice(0, MAX_FRECENCY_ENTRIES) setStore("data", Object.fromEntries(sorted)) const content = sorted.map(([path, entry]) => JSON.stringify({ path, ...entry })).join("\n") + "\n" - writeFile(frecencyPath, content).catch(() => {}) + writeFile(frecencyPath, content).catch((err) => + log.error("failed to write trimmed frecency file", { path: frecencyPath, err }), + ) } } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index b1616b95..a36630c1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -3,7 +3,7 @@ import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, S import "opentui-spinner/solid" import path from "path" import { fileURLToPath } from "url" -import { Filesystem } from "@/util" +import { Filesystem, Log } from "@/util" import { useLocal } from "@tui/context/local" import { tint, useTheme } from "@tui/context/theme" import { EmptyBorder, SplitBorder } from "@tui/component/border" @@ -46,6 +46,8 @@ import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { DialogAgreement, FREE_AGREEMENT_KEY, FREE_MODEL_IDS } from "../dialog-agreement" import { useArgs } from "@tui/context/args" +const log = Log.create({ service: "tui-prompt" }) + export type PromptProps = { sessionID?: string workspaceID?: string @@ -1063,7 +1065,7 @@ export function Prompt(props: PromptProps) { const res = await sdk.client.session.create({ workspace: props.workspaceID }) if (res.error) { - console.log("Creating a session failed:", res.error) + log.error("Creating a session failed", { error: res.error }) toast.show({ message: "Creating a session failed. Open console for more details.", diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx index 84ba6233..57ed9782 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx @@ -1,12 +1,14 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util" +import { Filesystem, Log } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import { createSimpleContext } from "../../context/helper" import { appendFile, writeFile } from "fs/promises" import type { PromptInfo } from "./history" +const log = Log.create({ service: "prompt-stash" }) + export type StashEntry = { input: string parts: PromptInfo["parts"] @@ -39,7 +41,9 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp // Rewrite file with only valid entries to self-heal corruption if (lines.length > 0) { const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(stashPath, content).catch(() => {}) + writeFile(stashPath, content).catch((err) => + log.error("failed to rewrite stash file on init", { path: stashPath, err }), + ) } }) @@ -66,11 +70,15 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp if (trimmed) { const content = store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(stashPath, content).catch(() => {}) + writeFile(stashPath, content).catch((err) => + log.error("failed to write trimmed stash file", { path: stashPath, err }), + ) return } - appendFile(stashPath, JSON.stringify(stash) + "\n").catch(() => {}) + appendFile(stashPath, JSON.stringify(stash) + "\n").catch((err) => + log.error("failed to append stash entry", { path: stashPath, err }), + ) }, pop() { if (store.entries.length === 0) return undefined @@ -82,7 +90,9 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp ) const content = store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "" - writeFile(stashPath, content).catch(() => {}) + writeFile(stashPath, content).catch((err) => + log.error("failed to write stash file after pop", { path: stashPath, err }), + ) return entry }, remove(index: number) { @@ -94,7 +104,9 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp ) const content = store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "" - writeFile(stashPath, content).catch(() => {}) + writeFile(stashPath, content).catch((err) => + log.error("failed to write stash file after remove", { path: stashPath, err }), + ) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 17cc0764..caffc30c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,5 +1,5 @@ import { Global } from "@/global" -import { Filesystem } from "@/util" +import { Filesystem, Log } from "@/util" import { Flock } from "@mimo-ai/shared/util/flock" import { rename, rm } from "fs/promises" import { createSignal, type Setter } from "solid-js" @@ -7,6 +7,8 @@ import { createStore, unwrap } from "solid-js/store" import { createSimpleContext } from "./helper" import path from "path" +const log = Log.create({ service: "tui-kv" }) + export const { use: useKV, provider: KVProvider } = createSimpleContext({ name: "KV", init: () => { @@ -34,7 +36,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ setStore(x) }) .catch((error) => { - console.error("Failed to read KV state", { filePath, error }) + log.error("Failed to read KV state", { filePath, error }) }) .finally(() => { setReady(true) @@ -67,7 +69,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ write = write .then(() => Flock.withLock(lock, () => writeSnapshot(snapshot))) .catch((error) => { - console.error("Failed to write KV state", { filePath, error }) + log.error("Failed to write KV state", { filePath, error }) }) }, delete(key: string) { @@ -77,7 +79,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ write = write .then(() => Flock.withLock(lock, () => writeSnapshot(snapshot))) .catch((error) => { - console.error("Failed to write KV state", { filePath, error }) + log.error("Failed to write KV state", { filePath, error }) }) }, } diff --git a/packages/opencode/src/cli/cmd/tui/plugin/slots.tsx b/packages/opencode/src/cli/cmd/tui/plugin/slots.tsx index 2c0159f8..237e7b8d 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/slots.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/slots.tsx @@ -1,6 +1,9 @@ import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@mimo-ai/plugin/tui" import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid" import { isRecord } from "@/util/record" +import { Log } from "@/util" + +const log = Log.create({ service: "tui-slots" }) type RuntimeSlotMap = TuiSlotMap> @@ -38,12 +41,12 @@ export function setupSlots(api: HostPluginApi): HostSlots { }, { onPluginError(event) { - console.error("[tui.slot] plugin error", { + log.error("plugin error", { plugin: event.pluginId, slot: event.slot, phase: event.phase, source: event.source, - message: event.error.message, + error: event.error.message, }) }, }, diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 8c535833..3fdf7fa9 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -5,6 +5,9 @@ import path from "path" import fs from "fs/promises" import * as Filesystem from "../../../../util/filesystem" import * as Process from "../../../../util/process" +import { Log } from "@/util" + +const log = Log.create({ service: "clipboard" }) // Lazy load which and clipboardy to avoid expensive execa/which/isexe chain at startup const getWhich = lazy(async () => { @@ -115,7 +118,7 @@ const getCopyMethod = lazy(async () => { const which = await getWhich() if (os === "darwin" && which("osascript")) { - console.log("clipboard: using osascript") + log.debug("using osascript") return async (text: string) => { const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true }) @@ -124,7 +127,7 @@ const getCopyMethod = lazy(async () => { if (os === "linux") { if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) { - console.log("clipboard: using wl-copy") + log.debug("using wl-copy") return async (text: string) => { const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) if (!proc.stdin) return @@ -134,7 +137,7 @@ const getCopyMethod = lazy(async () => { } } if (which("xclip")) { - console.log("clipboard: using xclip") + log.debug("using xclip") return async (text: string) => { const proc = Process.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe", @@ -148,7 +151,7 @@ const getCopyMethod = lazy(async () => { } } if (which("xsel")) { - console.log("clipboard: using xsel") + log.debug("using xsel") return async (text: string) => { const proc = Process.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe", @@ -164,7 +167,7 @@ const getCopyMethod = lazy(async () => { } if (os === "win32") { - console.log("clipboard: using powershell") + log.debug("using powershell") return async (text: string) => { // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) const proc = Process.spawn( @@ -189,7 +192,7 @@ const getCopyMethod = lazy(async () => { } } - console.log("clipboard: no native support") + log.debug("no native support, falling back to clipboardy") return async (text: string) => { const clipboardy = await getClipboardy() await clipboardy.write(text).catch(() => {})