Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 })
})
},
},
Expand Down Expand Up @@ -272,7 +274,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
config: tuiConfig,
})
.catch((error) => {
console.error("Failed to load TUI plugins", error)
log.error("Failed to load TUI plugins", { error })
})
.finally(() => {
setReady(true)
Expand Down
8 changes: 5 additions & 3 deletions packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 12 additions & 4 deletions packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }),
)
}
})

Expand All @@ -69,15 +73,19 @@ 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)
.sort(([, a], [, b]) => b.lastOpen - a.lastOpen)
.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 }),
)
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.",
Expand Down
24 changes: 18 additions & 6 deletions packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down Expand Up @@ -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 }),
)
}
})

Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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 }),
)
},
}
},
Expand Down
10 changes: 6 additions & 4 deletions packages/opencode/src/cli/cmd/tui/context/kv.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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"
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: () => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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 })
})
},
}
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/cli/cmd/tui/plugin/slots.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, object>>

Expand Down Expand Up @@ -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,
})
},
},
Expand Down
15 changes: 9 additions & 6 deletions packages/opencode/src/cli/cmd/tui/util/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 })
Expand All @@ -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
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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(
Expand All @@ -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(() => {})
Expand Down